Compare commits

..

28 Commits

Author SHA1 Message Date
f5a3b9eb42 Checkpoint Commit - Working larger test data and frontend polishing 2025-07-02 09:46:38 -05:00
461855dc3c Updated to ignore generated site files 2025-06-24 17:13:55 -05:00
1fd4780ea4 Added initial POC support for running at ./ instead of absolute / for the main dashboard 2025-06-24 16:55:46 -05:00
ad0b9698b1 Added initial POC support for running at ./ instead of absolute / for the main dashboard 2025-06-24 16:55:32 -05:00
d41f5f8339 added clarifying comment 2025-06-20 13:22:29 -05:00
4d30eb3060 Merge pull request #90 from Kingsrook/feature/search-possible-values-by-label
Feature/search possible values by label
2025-06-18 10:23:13 -05:00
d4a675e952 updated to include the unique count of valid values 2025-06-06 19:17:10 -05:00
633c97b710 fix when no helpContent avaliable 2025-06-03 17:27:11 -05:00
c70ef3dae8 feedback from review session 2025-06-02 16:39:27 -05:00
5c69ae666c added ability to search for possible value data using the PVS labels, rather than just the ids, updated the values paster widget thingy to use this change to make pvs requests in a paginated manner 2025-05-27 15:17:57 -05:00
2e5aba6c16 Merge tag 'version-0.25.0' into dev
Tag release
2025-05-20 07:52:28 -05:00
185775ca4d Merge branch 'release/0.25.0' 2025-05-20 07:52:28 -05:00
cbcb3b505e Update for next development version 2025-05-20 07:06:37 -05:00
ce91f68088 Update versions for release 2025-05-20 07:06:35 -05:00
81da1a4627 Merged feature/oauth2-authentication-module into dev 2025-05-19 20:33:35 -05:00
b279a04b43 quick bug fix for goto fields 2025-04-16 16:45:46 -05:00
1f2e57d688 Merged feature/better-goto-behavior into dev 2025-04-09 11:14:14 -05:00
52bb7ba411 Merged feature/disable-show-default-vs-display-value into dev 2025-04-09 11:14:02 -05:00
34c6f650b5 updated to handle (ignore) fields with empty strings when using goto dialog 2025-04-07 16:50:01 -05:00
d792c23035 Cleanup from code review 2025-04-05 19:58:35 -05:00
e3d30633f1 Refactor authentication handling to pass authentication metadata into App.
eliminates warnings from oauth2 hook by conditionally using its useAuth hook.
2025-04-05 19:37:02 -05:00
a6ee682671 Merged feature/dk-misc-20250318 into dev 2025-04-03 14:28:34 -05:00
c62252075f Merged feature/banners into dev 2025-04-03 14:26:13 -05:00
debc6f3ebf turn off replacing of displayValue with defaultValue 2025-04-02 12:10:39 -05:00
679375ba63 update processClicked to set alert if min/max input records isn't satisfied 2025-03-18 11:44:32 -05:00
fb10dad803 Add support for query-param defaultProcessValues (as a json object) 2025-03-18 11:40:22 -05:00
c9a618c7f6 Fix full-width file upload adornment for lg-size (regressed with field-level grid columns addition) 2025-03-10 12:12:37 -05:00
13ce684d23 Initial checkin of Banners under QBrandingMetaData
- includes migration from (now deprecated) MetaDataFilterInterface to MetaDataActionCustomizerInterface (stored on the QInstance and used by MetaDataAction)
- includes migration from (now deprecated) environmentBannerText and environmentBannerColor in QBrandingMetaData to now be implemented as a banner
2025-03-07 14:58:51 -06:00
29 changed files with 4985 additions and 19881 deletions

2
.gitignore vendored
View File

@ -5,6 +5,7 @@
.yalc*
yalc.lock
.env
/certs
# dependencies
/node_modules
@ -30,3 +31,4 @@ yalc.lock
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/src/main/resources/material-dashboard/

View File

@ -154,7 +154,7 @@ material-dashboard-2-pro-react-ts
│ │   ├── Cards
│ │   ├── Charts
│ │   ├── Configurator
│ │   ├── Footer
│ │   ├── FooterCard
│ │   ├── Items
│ │   ├── LayoutContainers
│ │   ├── Lists

23732
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.118",
"@kingsrook/qqq-frontend-core": "1.0.122",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
@ -23,6 +23,8 @@
"@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
"ajv": "^8.11.0",
"ajv-keywords": "^5.1.0",
"chart.js": "3.4.1",
"chroma-js": "2.4.2",
"cmdk": "0.2.0",
@ -36,8 +38,8 @@
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1",
"react-oidc-context": "2.3.1",
"rapidoc": "9.3.4",
"react": "18.0.0",
"react-ace": "10.1.0",
@ -51,6 +53,7 @@
"react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1",
"react-oidc-context": "2.3.1",
"react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3",
"react-table": "7.7.0",
@ -59,14 +62,15 @@
"yup": "0.32.11"
},
"scripts": {
"build": "react-scripts build",
"build": "PUBLIC_URL=. react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test"
"start": "PUBLIC_URL=. BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test",
"export": "rm -rf dist && PUBLIC_URL=. react-scripts build && rm -rf src/main/resources/material-dashboard && mkdir -p src/main/resources/material-dashboard && cp -r build/* src/main/resources/material-dashboard"
},
"eslintConfig": {
"extends": [

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging>
<properties>
<revision>0.25.0-SNAPSHOT</revision>
<revision>0.26.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -66,7 +66,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>0.21.0</version>
<version>0.26.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>

View File

@ -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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon";
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 Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
import AppHome from "qqq/pages/apps/Home";
import NoApps from "qqq/pages/apps/NoApps";
@ -63,9 +65,14 @@ import {Md5} from "ts-md5/dist/md5";
const qController = Client.getInstance();
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 [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
const [profileRoutes, setProfileRoutes] = useState({});
@ -74,11 +81,10 @@ export default function App()
const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
const [authenticationMetaData, setAuthenticationMetaData] = useState(null as QAuthenticationMetaData | null);
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext: authenticationMetaData.type === "OAUTH2"});
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
/////////////////////////////////////////////////////////
@ -99,9 +105,6 @@ export default function App()
(async () =>
{
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
setAuthenticationMetaData(authenticationMetaData);
if (authenticationMetaData.type === "AUTH_0")
{
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()
{
@ -157,7 +160,7 @@ export default function App()
}
const [controller, dispatch] = useMaterialUIController();
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
const {miniSidenav, direction, sidenavColor} = controller;
const [onMouseEnter, setOnMouseEnter] = useState(false);
const {pathname} = useLocation();
const [queryParams] = useSearchParams();
@ -450,11 +453,10 @@ export default function App()
}
}
let profileRoutes = {};
const gravatarBase = "https://www.gravatar.com/avatar/";
const hash = Md5.hashStr(loggedInUser?.email || "user");
const profilePicture = `${gravatarBase}${hash}`;
profileRoutes = {
const profileRoutes = {
type: "collapse",
name: loggedInUser?.name ?? "Anonymous",
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 (
appRoutes && (
@ -657,6 +676,7 @@ export default function App()
<ThemeProvider theme={theme}>
<CssBaseline />
<CommandMenu metaData={metaData} />
{banner()}
<Sidenav
color={sidenavColor}
icon={branding.icon}

View File

@ -36,6 +36,20 @@ import {BrowserRouter} from "react-router-dom";
const qController = Client.getInstance();
function getBasePath(): string
{
// You can change this logic depending on how you detect your mount point
const path = window.location.pathname;
console.warn("Using hacked base path for QQQ application, please update this code to be better : path ["+ path +"].");
// Example: If app is deployed at /admin or /portal
if (path.startsWith("/admin")) return "/admin";
if (path.startsWith("/portal")) return "/portal"; // TODO: This is all temporary, we need to fix this properly
return "/";
}
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
{
qController.clearAuthenticationMetaDataLocalStorage();
@ -52,7 +66,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
function Auth0RouterBody()
{
const {renderAppWrapper} = useAuth0AuthenticationModule({});
return (renderAppWrapper(authenticationMetaData, null));
return (renderAppWrapper(authenticationMetaData));
}
@ -61,10 +75,10 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
***************************************************************************/
function OAuth2RouterBody()
{
const {renderAppWrapper} = useOAuth2AuthenticationModule({});
const {renderAppWrapper} = useOAuth2AuthenticationModule({inOAuthContext: false});
return (renderAppWrapper(authenticationMetaData, (
<MaterialUIControllerProvider>
<App />
<App authenticationMetaData={authenticationMetaData} />
</MaterialUIControllerProvider>
)));
}
@ -78,7 +92,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
return (renderAppWrapper(authenticationMetaData, (
<MaterialUIControllerProvider>
<App />
<App authenticationMetaData={authenticationMetaData} />
</MaterialUIControllerProvider>
)));
}
@ -89,19 +103,19 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
if (authenticationMetaData.type === "AUTH_0")
{
root.render(<BrowserRouter>
root.render(<BrowserRouter basename={getBasePath()}>
<Auth0RouterBody />
</BrowserRouter>);
}
else if (authenticationMetaData.type === "OAUTH2")
{
root.render(<BrowserRouter>
root.render(<BrowserRouter basename={getBasePath()}>
<OAuth2RouterBody />
</BrowserRouter>);
}
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
{
root.render(<BrowserRouter>
root.render(<BrowserRouter basename={getBasePath()}>
<AnonymousRouterBody />
</BrowserRouter>);
}

View File

@ -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
}

View File

@ -41,11 +41,11 @@ interface Props
/***************************************************************************
** 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 [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))
{
console.log("Sending accessToken to backend, requesting a sessionUUID...");
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
const {uuid: values} = await qController.manageSession(accessToken, null);
localStorage.setItem("accessToken", accessToken);
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
let domain: string = authenticationMetaData.data.baseUrl;
@ -225,6 +220,15 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
domain = domain.replace(/\/$/, "");
}
/***************************************************************************
** simple Functional Component to wrap the <App> and pass the authentication-
** MetaData prop in, so a simple Component can be passed into ProtectedRoute
***************************************************************************/
function WrappedApp()
{
return <App authenticationMetaData={authenticationMetaData} />
}
return (
<Auth0ProviderWithRedirectCallback
domain={domain}
@ -232,7 +236,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
audience={audience}
redirectUri={`${window.location.origin}/`}>
<MaterialUIControllerProvider>
<ProtectedRoute component={App} />
<ProtectedRoute component={WrappedApp} />
</MaterialUIControllerProvider>
</Auth0ProviderWithRedirectCallback>
);

View File

@ -23,7 +23,7 @@ import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/me
import {SESSION_UUID_COOKIE_NAME} from "App";
import Client from "qqq/utils/qqq/Client";
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";
const qController = Client.getInstance();
@ -33,16 +33,22 @@ interface Props
setIsFullyAuthenticated?: (is: boolean) => void;
setLoggedInUser?: (user: any) => void;
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
inOAuthContext: boolean;
}
/***************************************************************************
** hook for working with the OAuth2 authentication module
***************************************************************************/
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: 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 navigate = useNavigate();
@ -56,6 +62,12 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
if (window.location.pathname == "/token")
{
///////////////////////////////////////////////////////////////////////////
// if we're at a path of /token, get code & state params, look up values //
// from that state in local storage, and make a post to the backend to //
// with these values - which will itself talk to the identity provider //
// to get an access token, and ultimately a session. //
///////////////////////////////////////////////////////////////////////////
const code = searchParams.get("code");
const state = searchParams.get("state");
const oidcString = localStorage.getItem(`oidc.${state}`);
@ -77,14 +89,24 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
localStorage.removeItem(preSigninRedirectPathname);
navigate(preSigninRedirectPathname ?? "/", {replace: true});
}
else
{
////////////////////////////////////////////
// if unrecognized state, render an error //
////////////////////////////////////////////
setEarlyReturnForAuth(<div>Login error: Unrecognized state. Refresh to try again.</div>);
}
}
else
{
//////////////////////////////////////////////////////////////////////////
// if we have a sessionUUID cookie, try to validate it with the backend //
//////////////////////////////////////////////////////////////////////////
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
if (sessionUuid)
{
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
const {uuid: newSessionUuid, values} = await qController.manageSession(null, sessionUuid, null);
const {values} = await qController.manageSession(null, sessionUuid, null);
setIsFullyAuthenticated(true);
qController.setGotAuthentication();
@ -94,45 +116,16 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// else no cookie, and not a token url, we need to redirect to the provider's login page //
// capture the path the user was trying to access in local storage, to redirect back to later. //
/////////////////////////////////////////////////////////////////////////////////////////////////
console.log("Loading token from OAuth2 provider...");
console.log(authOidc);
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
setEarlyReturnForAuth(<div>Signing in...</div>);
authOidc.signinRedirect();
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)
@ -151,7 +144,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
{
qController.clearAuthenticationMetaDataLocalStorage();
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
authOidc.signoutRedirect();
authOidc?.signoutRedirect();
};

View File

@ -23,8 +23,10 @@ import {Chip} from "@mui/material";
import TextField from "@mui/material/TextField";
import {makeStyles} from "@mui/styles";
import Downshift from "downshift";
import {debounce} from "lodash";
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) => ({
chip: {
@ -34,21 +36,99 @@ const useStyles = makeStyles((theme: any) => ({
function ChipTextField({...props})
{
const qController = Client.getInstance();
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 [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(() =>
{
setChips(chipData);
}, [chipData]);
chipsRef.current = chipData;
determineChipColors();
}, [JSON.stringify(chipData)]);
useEffect(() =>
{
handleChipChange(chips);
}, [chips, handleChipChange]);
handleChipChange(isMakingRequest, chipValidity, chipPVSIds);
}, [chipValidity, chipPVSIds, isMakingRequest]);
function handleKeyDown(event: any)
{
@ -64,13 +144,16 @@ function ChipTextField({...props})
setInputValue("");
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());
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));
}
@ -87,18 +170,26 @@ function ChipTextField({...props})
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>; }; })
{
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 (
<React.Fragment>
@ -116,7 +207,7 @@ function ChipTextField({...props})
});
// @ts-ignore
return (
<div id="chip-text-field-container" style={{flexWrap: "wrap", display:"flex"}}>
<div id="chip-text-field-container" style={{flexWrap: "wrap", display: "flex"}}>
<TextField
sx={{width: "99%"}}
disabled={disabled}
@ -125,16 +216,16 @@ function ChipTextField({...props})
startAdornment:
<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
color={(chipType !== "number" || ! Number.isNaN(Number(item))) ? "info" : "error"}
key={`${item}-${i}`}
onChange={determineChipColors}
color={chipColors[index]}
key={`${item}-${index}`}
variant="outlined"
tabIndex={-1}
label={item}
className={classes.chip}
/>
))
}
</div>,
@ -158,6 +249,7 @@ function ChipTextField({...props})
</React.Fragment>
);
}
ChipTextField.defaultProps = {
chipData: []
};
@ -166,4 +258,4 @@ ChipTextField.propTypes = {
chipData: arrayOf(string)
};
export default ChipTextField
export default ChipTextField;

View File

@ -96,6 +96,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
if (width == "full")
{
itemSM = 12;
itemLG = 12;
}
return (

View File

@ -20,17 +20,18 @@
*/
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 {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 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 MDTypography from "qqq/components/legacy/MDTypography";
import React, {useMemo, useState} from "react";
import AceEditor from "react-ace";
import {flushSync} from "react-dom";
// Declaring props types for FormField
@ -83,10 +84,10 @@ function QDynamicFormField({
if (placeholder)
{
inputProps.placeholder = placeholder
inputProps.placeholder = placeholder;
}
if(backgroundColor)
if (backgroundColor)
{
inputProps.sx = {
"&.MuiInputBase-root": {
@ -124,7 +125,7 @@ function QDynamicFormField({
{
onChange.onChange = (e: any) =>
{
if(isToUpperCase || isToLowerCase)
if (isToUpperCase || isToLowerCase)
{
const beforeStart = e.target.selectionStart;
const beforeEnd = e.target.selectionEnd;
@ -141,7 +142,7 @@ function QDynamicFormField({
newValue = newValue.toLowerCase();
}
setFieldValue(name, newValue);
if(onChangeCallback)
if (onChangeCallback)
{
onChangeCallback(newValue);
}
@ -153,7 +154,7 @@ function QDynamicFormField({
input.setSelectionRange(beforeStart, beforeEnd);
}
}
else if(onChangeCallback)
else if (onChangeCallback)
{
onChangeCallback(e.currentTarget.value);
}
@ -165,15 +166,15 @@ function QDynamicFormField({
***************************************************************************/
function dynamicSelectOnChange(newValue?: QPossibleValue)
{
if(onChangeCallback)
if (onChangeCallback)
{
onChangeCallback(newValue == null ? null : newValue.id)
onChangeCallback(newValue == null ? null : newValue.id);
}
}
let field;
let getsBulkEditHtmlLabel = true;
if(formFieldObject.possibleValueProps)
if (formFieldObject.possibleValueProps)
{
field = (<DynamicSelect
name={name}
@ -186,7 +187,7 @@ function QDynamicFormField({
onChange={dynamicSelectOnChange}
// otherValues={otherValuesMap}
useCase="form"
/>)
/>);
}
else if (type === "checkbox")
{
@ -220,7 +221,7 @@ function QDynamicFormField({
onChange={(value: string, event: any) =>
{
setFieldValue(name, value, false);
if(onChangeCallback)
if (onChangeCallback)
{
onChangeCallback(value);
}

View File

@ -174,7 +174,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
{
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[]> =>
{
if(possibleValues)
if (possibleValues)
{
return filterInlinePossibleValues(searchTerm, possibleValues)
return filterInlinePossibleValues(searchTerm, possibleValues);
}
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);
}
}
};
/***************************************************************************

View File

@ -184,9 +184,9 @@ function EntityForm(props: Props): JSX.Element
///////////////////////////////////////////////////////////////////////////////////////
// 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];
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. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const displayValues: {[fieldName: string]: string} = {};
if(childTableName && values)
const displayValues: { [fieldName: string]: string } = {};
if (childTableName && values)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
///////////////////////////////////////////////////////////////////////////////////////////////////////
const childTableMetaData = await qController.loadTableMetaData(childTableName)
const childTableMetaData = await qController.loadTableMetaData(childTableName);
for (let key in values)
{
const value = values[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")
if(possibleValues && possibleValues.length > 0)
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], null, objectToMap(values), "form");
if (possibleValues && possibleValues.length > 0)
{
displayValues[key] = possibleValues[0].label;
}
@ -516,13 +516,12 @@ function EntityForm(props: Props): JSX.Element
}
/***************************************************************************
**
***************************************************************************/
function objectToMap(object: { [key: string]: any }): Map<string, any>
{
if(object == null)
if (object == null)
{
return (null);
}
@ -532,7 +531,7 @@ function EntityForm(props: Props): JSX.Element
{
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;
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)
{
defaultDisplayValues.set(fieldName, results[0].label);

View File

@ -32,6 +32,7 @@ import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
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 {ReactNode, useEffect, useReducer, useState} from "react";
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 (
<SidenavRoot
{...rest}
@ -331,12 +356,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
</Box>
}
</Box>
{
branding && branding.environmentBannerText &&
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
{branding.environmentBannerText}
</Box>
}
<EnvironmentBanner branding={branding} />
</Box>
<Divider
light={

View File

@ -97,6 +97,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
margin: "0",
borderRadius: "0",
height: "100%",
top: "unset",
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
},

View 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}</>;
}

View File

@ -20,6 +20,7 @@
*/
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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
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 Icon from "@mui/material/Icon";
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 MDButton from "qqq/components/legacy/MDButton";
import Client from "qqq/utils/qqq/Client";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
interface Props
{
@ -162,8 +162,8 @@ function GotoRecordDialog(props: Props): JSX.Element
/***************************************************************************
** event handler for close button
***************************************************************************/
** event handler for close button
***************************************************************************/
const closeRequested = () =>
{
if (props.mayClose)
@ -182,23 +182,23 @@ function GotoRecordDialog(props: Props): JSX.Element
options[optionIndex].forEach((field) =>
{
if(values[field.name])
if (values[field.name])
{
anyFieldsInThisOptionHaveAValue = true;
}
})
});
if(!anyFieldsInThisOptionHaveAValue)
if (!anyFieldsInThisOptionHaveAValue)
{
return (true);
}
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) =>
{
setError("");
@ -207,9 +207,13 @@ function GotoRecordDialog(props: Props): JSX.Element
const queryStringParts: string[] = [];
options[optionIndex].forEach((field) =>
{
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
})
if (field.type == QFieldType.STRING && !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);
@ -223,7 +227,7 @@ function GotoRecordDialog(props: Props): JSX.Element
}
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 //

View File

@ -26,7 +26,6 @@ import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import RadioGroup from "@mui/material/RadioGroup";
import TextField from "@mui/material/TextField";
import {useFormikContext} from "formik";
@ -94,7 +93,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
// deal with dynamically loading the initial default value for a possible value... //
/////////////////////////////////////////////////////////////////////////////////////
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
if(dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
if (dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
{
actuallyDoingInitialLoadOfPossibleValue = true;
setDoingInitialLoadOfPossibleValue(true);
@ -104,7 +103,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
{
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)
{
setPossibleValueInitialDisplayValue(possibleValues[0].label);
@ -114,9 +113,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
setPossibleValueInitialDisplayValue(null);
}
}
catch(e)
catch (e)
{
console.log(`Error loading possible value: ${e}`)
console.log(`Error loading possible value: ${e}`);
}
actuallyDoingInitialLoadOfPossibleValue = false;
@ -124,7 +123,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
})();
}
if(dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
if (dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
{
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
}
@ -134,11 +133,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
// don't allow duplicates //
//////////////////////////////////////////////////////
const columnOptions: { value: number, label: string }[] = [];
const usedLabels: {[label: string]: boolean} = {};
const usedLabels: { [label: string]: boolean } = {};
for (let i = 0; i < columnNames.length; i++)
{
const label = columnNames[i];
if(!usedLabels[label])
if (!usedLabels[label])
{
columnOptions.push({label: label, value: i});
usedLabels[label] = true;
@ -148,9 +147,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
//////////////////////////////////////////////////////////////////////
// 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]);
}

View File

@ -19,6 +19,10 @@
* 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 Box from "@mui/material/Box";
import Card from "@mui/material/Card";
@ -28,20 +32,26 @@ import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
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 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
{
type: string;
onSave: (newValues: any[]) => void;
table?: QTableMetaData;
field?: QFieldMetaData;
}
FilterCriteriaPaster.defaultProps = {};
const qController = Client.getInstance();
function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
{
enum Delimiter
{
@ -68,6 +78,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
mainCardStyles.width = "60%";
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);
const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false);
const [inputText, setInputText] = useState("");
@ -75,8 +91,13 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
const [delimiterCharacter, setDelimiterCharacter] = useState("");
const [customDelimiterValue, setCustomDelimiterValue] = useState("");
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 [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 //
@ -92,6 +113,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
setDelimiter("");
setDelimiterCharacter("");
setChipData([]);
setChipValidity([]);
setInputText("");
setDetectedText("");
setCustomDelimiterValue("");
@ -106,18 +128,43 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
const handleSaveClicked = () =>
{
////////////////////////////////////////
// if numeric remove any non-numerics //
////////////////////////////////////////
///////////////////////////////////////////////////////////////
// if numeric remove any non-numerics, or invalid pvs values //
///////////////////////////////////////////////////////////////
let saveData = [];
let usedLabels = new Map<any, boolean>();
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);
clearData();
@ -214,6 +261,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
useEffect(() =>
{
(async () =>
{
const metaData = await qController.loadMetaData();
setMetaData(metaData);
})();
let currentDelimiter = delimiter;
let currentDelimiterCharacter = delimiterCharacter;
@ -246,10 +299,16 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
let parts = inputText.split(regex);
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 //
///////////////////////////////////////////////////////
setErrorText("");
let invalidCount = 0;
if (currentDelimiterCharacter !== "")
{
for (let i = 0; i < parts.length; i++)
@ -259,20 +318,47 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
{
chipData.push(part);
///////////////////////////////////////////////////////////
// if numeric, check that first before pushing as a chip //
///////////////////////////////////////////////////////////
if (type === "number" && Number.isNaN(Number(part)))
////////////////////////////////////////////////////////////////
// if numeric or pvs, check validity and add to invalid count //
////////////////////////////////////////////////////////////////
if (chipValidity[i] != null && chipValidity[i] !== true)
{
setErrorText("Some values are not numbers");
if ((type === "number" && Number.isNaN(Number(part))) || type === "pvs")
{
invalidCount++;
}
}
else
{
let count = uniqueValuesMap[part] == null ? 0 : uniqueValuesMap[part];
uniqueValuesMap[part] = count + 1;
}
}
}
}
if (invalidCount > 0)
{
if (type === "number")
{
let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers";
setErrorText(invalidCount + suffix + "numbers and will not be added to the filter");
}
else if (type === "pvs")
{
let suffix = invalidCount === 1 ? " value was" : " values were";
setErrorText(invalidCount + suffix + " not found and will not be added to the filter");
}
}
setUniqueCount(Object.keys(uniqueValuesMap).length);
setChipData(chipData);
}, [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 (
<Box>
@ -283,128 +369,155 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
pasteModalIsOpen &&
(
<Modal open={pasteModalIsOpen}>
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
<Card sx={mainCardStyles}>
<Box p={4} pb={2}>
<Grid container>
<Grid item pr={3} xs={12} lg={12}>
<Typography variant="h5">Bulk Add Filter Values</Typography>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
Paste into the box on the left.
Review the filter values in the box on the right.
If the filter values are not what are expected, try changing the separator using the dropdown below.
</Typography>
<Box>
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
<Card sx={mainCardStyles}>
<Box p={4} pb={2}>
<Grid container>
<Grid item pr={3} xs={12} lg={12}>
<Typography variant="h5">Bulk Add Filter Values</Typography>
{
formattedHelpContent && <Box sx={{display: "flex", lineHeight: "1.7", textTransform: "none"}}>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
{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
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>
</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
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={() =>
{
}}
chipData={chipData}
chipType={type}
multiline
fullWidth
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}}
/>
<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>
)}
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
{delimiter === Delimiter.CUSTOM.valueOf() && (
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}>
<i>{detectedText}</i>
</Typography>
)}
</Box>
<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>
)}
{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 sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}>
{
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>
)
}
</Grid>
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}>
{
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 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 Values" disabled={saveDisabled} />
</Grid>
</Box>
</Card>
</Box>
</Box>
</Box>
</Modal>

View File

@ -398,20 +398,25 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
initialValues = criteria.values;
}
}
return <Box>
<DynamicSelect
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
overrideId={field.name + "-multi-" + criteria.id}
key={field.name + "-multi-" + criteria.id}
isMultiple
fieldLabel="Values"
initialValues={initialValues}
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard"
useCase="filter"
/>
return <Box display="flex" alignItems="flex-end" className="multiValue">
<Box width={"100%"}>
<DynamicSelect
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
overrideId={field.name + "-multi-" + criteria.id}
key={field.name + "-multi-" + criteria.id + "-" + criteria.values.length}
isMultiple
fieldLabel="Values"
initialValues={initialValues}
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard"
useCase="filter"
/>
</Box>
<Box>
<FilterCriteriaPaster table={table} field={field} type="pvs" onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
</Box>
</Box>;
}

View File

@ -38,7 +38,7 @@ import XIcon from "qqq/components/query/XIcon";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
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";
@ -135,7 +135,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
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:");
try
@ -144,7 +144,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
console.log("Criteria: " + JSON.stringify(criteria));
console.log("Default Operator: " + JSON.stringify(defaultOperator));
}
catch(e)
catch (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);
useEffect(() =>
{
//////////////////////////////////////////////////////////////////////////////
// was not seeing criteria changes take place until watching it stringified //
//////////////////////////////////////////////////////////////////////////////
setCriteria(criteria);
}, [JSON.stringify(criteria)]);
/*******************************************************************************
**

View File

@ -21,11 +21,12 @@
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import Box from "@mui/material/Box";
import {ReactNode, useEffect, useState} from "react";
import Footer from "qqq/components/horseshoe/Footer";
import NavBar from "qqq/components/horseshoe/NavBar";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import DashboardLayout from "qqq/layouts/DashboardLayout";
import Client from "qqq/utils/qqq/Client";
import {ReactNode, useEffect, useState} from "react";
interface Props
{
@ -80,12 +81,34 @@ function BaseLayout({stickyNavbar, children}: Props): JSX.Element
return () => window.removeEventListener("resize", handleTabsOrientation);
}, [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 (
<DashboardLayout>
<NavBar />
<Box>{children}</Box>
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
</DashboardLayout>
<>
<DashboardLayout>
{banner()}
<NavBar />
<Box>{children}</Box>
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
</DashboardLayout>
</>
);
}

View File

@ -1838,6 +1838,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
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)
{
for (let key in defaultProcessValues)

View File

@ -1612,6 +1612,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
*******************************************************************************/
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.
// alternatively, let a process itself have an initial screen to select rows...
openModalProcess(process);

View File

@ -748,35 +748,54 @@ input[type="search"]::-webkit-search-results-decoration
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);
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;
}
.helpContentAlert.warning
.helpContentAlert.warning,
.banner.warning
{
background-color: rgb(254, 245, 234);
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;
}
.helpContentAlert.error
.helpContentAlert.error,
.banner.error
{
background-color: rgb(254, 239, 238);
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;
}

View File

@ -133,7 +133,7 @@ class FilterUtils
}
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");
}
}

View File

@ -267,7 +267,13 @@ class ValueUtils
{
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)