mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-19 13:50:43 +00:00
Compare commits
97 Commits
snapshot-f
...
dev
Author | SHA1 | Date | |
---|---|---|---|
a588d3b1c6 | |||
5c2bf9e7b4 | |||
6a004d6cdb | |||
92a71bc62f | |||
2c65826a91 | |||
86dcc90e1d | |||
90fd03ae46 | |||
1dce760934 | |||
ff4683af1f | |||
ab4be1d5af | |||
0d7e76df6c | |||
d41f5f8339 | |||
f0f09a8ff1 | |||
4d30eb3060 | |||
d4a675e952 | |||
633c97b710 | |||
c70ef3dae8 | |||
6f15356b51 | |||
0bf33a01f9 | |||
0bca8e9361 | |||
6b90894425 | |||
248040a99f | |||
80ac2a304a | |||
b82b25156e | |||
69b46570cb | |||
3da656c01f | |||
1da0f4f1de | |||
ce947bc0f7 | |||
0a42b9d4f0 | |||
5ab906bcfe | |||
c1ea7081f1 | |||
020e174110 | |||
68c1f897af | |||
7d6b083ae2 | |||
3d4f0ba24b | |||
6fc11bb0ba | |||
78c788812a | |||
cb36f59090 | |||
96bdcf1874 | |||
07d116d9ba | |||
5c69ae666c | |||
5bdc3a6cd0 | |||
2e5aba6c16 | |||
185775ca4d | |||
cbcb3b505e | |||
ce91f68088 | |||
81da1a4627 | |||
bb06e2743a | |||
b279a04b43 | |||
1f2e57d688 | |||
52bb7ba411 | |||
34c6f650b5 | |||
d792c23035 | |||
e3d30633f1 | |||
a6ee682671 | |||
c62252075f | |||
debc6f3ebf | |||
679375ba63 | |||
fb10dad803 | |||
c9a618c7f6 | |||
f654208769 | |||
3dacab8d60 | |||
13ce684d23 | |||
b67eea7d87 | |||
8ae3b95105 | |||
5a309e5628 | |||
67e1e78817 | |||
214b6b8af4 | |||
8ec0ce5455 | |||
07cb6fd323 | |||
3bb8451671 | |||
6076c4ddfd | |||
44a8810260 | |||
c69a4b8203 | |||
7db4f34ddd | |||
71dc3f3f65 | |||
ce22db2f89 | |||
aacb239164 | |||
219458ec63 | |||
59fdc72455 | |||
5c3ddb7dec | |||
d65c1fb5d8 | |||
19a63d6956 | |||
40f5b55307 | |||
7320b19fbb | |||
3f8a3e7e4d | |||
3ef2d64327 | |||
d793c23861 | |||
d0201d96e1 | |||
7b66ece466 | |||
02c163899a | |||
8fafe16a95 | |||
722c8d3bcf | |||
85acb612c9 | |||
74c634414a | |||
f8368b030c | |||
dda4ea4f4b |
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@
|
||||
.yalc*
|
||||
yalc.lock
|
||||
.env
|
||||
/certs
|
||||
|
||||
# dependencies
|
||||
/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",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.113",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.124",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
@ -36,6 +36,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",
|
||||
"rapidoc": "9.3.4",
|
||||
"react": "18.0.0",
|
||||
"react-ace": "10.1.0",
|
||||
@ -49,6 +51,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",
|
||||
|
10
pom.xml
10
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.24.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,13 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.21.0</version>
|
||||
<version>0.26.0-integration-20250529-234230</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-middleware-javalin</artifactId>
|
||||
<optional>true</optional>
|
||||
<version>0.26.0-integration-20250529-234230</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
298
src/App.tsx
298
src/App.tsx
@ -19,7 +19,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
||||
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
||||
@ -29,16 +28,20 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {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";
|
||||
import {LicenseInfo} from "@mui/x-license-pro";
|
||||
import CommandMenu from "CommandMenu";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import QContext from "QContext";
|
||||
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||
import theme from "qqq/components/legacy/Theme";
|
||||
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context";
|
||||
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
||||
import AppHome from "qqq/pages/apps/Home";
|
||||
import NoApps from "qqq/pages/apps/NoApps";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
@ -62,10 +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]);
|
||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
||||
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,68 +81,20 @@ 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 [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
||||
|
||||
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext: authenticationMetaData.type === "OAUTH2"});
|
||||
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// tell the client how to do a logout if it sees a 401 //
|
||||
/////////////////////////////////////////////////////////
|
||||
Client.setUnauthorizedCallback(() =>
|
||||
{
|
||||
logout();
|
||||
});
|
||||
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||
{
|
||||
console.log("No session uuid cookie - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
if (!oldToken)
|
||||
{
|
||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const oldJSON: any = jwt_decode(oldToken);
|
||||
const newJSON: any = jwt_decode(newToken);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the old (local storage) token is expired, then we need to store the new one //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
const oldExp = oldJSON["exp"];
|
||||
if (oldExp * 1000 < (new Date().getTime()))
|
||||
{
|
||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
delete newJSON["exp"];
|
||||
delete newJSON["iat"];
|
||||
delete oldJSON["exp"];
|
||||
delete oldJSON["iat"];
|
||||
|
||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||
if (different)
|
||||
{
|
||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e);
|
||||
}
|
||||
|
||||
return (true);
|
||||
};
|
||||
Client.setUnauthorizedCallback(() => doLogout());
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// deal with making sure user is authenticated //
|
||||
/////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
if (loadingToken)
|
||||
@ -146,65 +105,17 @@ export default function App()
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
||||
|
||||
if (authenticationMetaData.type === "AUTH_0")
|
||||
{
|
||||
/////////////////////////////////////////
|
||||
// use auth0 if auth type is ... auth0 //
|
||||
/////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
console.log("Loading token from auth0...");
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
|
||||
const lsAccessToken = localStorage.getItem("accessToken");
|
||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||
{
|
||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
||||
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
|
||||
setLoggedInUser(user);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
await auth0SetupSession();
|
||||
}
|
||||
else if (authenticationMetaData.type === "OAUTH2")
|
||||
{
|
||||
await oauth2SetupSession();
|
||||
}
|
||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// use a random token if anonymous or mock //
|
||||
/////////////////////////////////////////////
|
||||
console.log("Generating random token...");
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
return;
|
||||
await anonymousSetupSession();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -220,13 +131,36 @@ export default function App()
|
||||
(async () =>
|
||||
{
|
||||
const metaData: QInstance = await qController.loadMetaData();
|
||||
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||
LicenseInfo.setLicenseKey(metaData.environmentValues?.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||
setNeedLicenseKey(false);
|
||||
})();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** call appropriate logout function based on authentication meta data type
|
||||
***************************************************************************/
|
||||
function doLogout()
|
||||
{
|
||||
if (authenticationMetaData?.type === "AUTH_0")
|
||||
{
|
||||
auth0Logout();
|
||||
}
|
||||
else if (authenticationMetaData?.type === "OAUTH2")
|
||||
{
|
||||
oauth2Logout();
|
||||
}
|
||||
else if (authenticationMetaData?.type === "FULLY_ANONYMOUS" || authenticationMetaData?.type === "MOCK")
|
||||
{
|
||||
anonymousLogout();
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`No logout callback for authentication type [${authenticationMetaData?.type}].`);
|
||||
}
|
||||
}
|
||||
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
||||
const {miniSidenav, direction, sidenavColor} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const {pathname} = useLocation();
|
||||
const [queryParams] = useSearchParams();
|
||||
@ -443,23 +377,57 @@ export default function App()
|
||||
});
|
||||
});
|
||||
|
||||
const runRecordScriptProcess = metaData.processes.get("runRecordScript");
|
||||
if (runRecordScriptProcess)
|
||||
const materialDashboardInstanceMetaData = metaData.supplementalInstanceMetaData?.get("materialDashboard");
|
||||
if (materialDashboardInstanceMetaData)
|
||||
{
|
||||
const process = runRecordScriptProcess;
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: process.name,
|
||||
route: `${path}/${process.name}`,
|
||||
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||
});
|
||||
const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
|
||||
if (processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
for (let processName of processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
const process = metaData.processes.get(processName);
|
||||
if (process)
|
||||
{
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: process.name,
|
||||
route: `${path}/${process.name}`,
|
||||
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: `${app.name}/${process.name}`,
|
||||
route: `${path}/:id/${process.name}`,
|
||||
component: <RecordView table={table} launchProcess={process} />,
|
||||
});
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: `${app.name}/${process.name}`,
|
||||
route: `${path}/:id/${process.name}`,
|
||||
component: <RecordView table={table} launchProcess={process} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////
|
||||
// deprecated //
|
||||
////////////////
|
||||
const runRecordScriptProcess = metaData.processes.get("runRecordScript");
|
||||
if (runRecordScriptProcess)
|
||||
{
|
||||
const process = runRecordScriptProcess;
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: process.name,
|
||||
route: `${path}/${process.name}`,
|
||||
component: <RecordQuery table={table} key={`${table.name}-${process.name}`} launchProcess={process} />,
|
||||
});
|
||||
|
||||
routeList.push({
|
||||
name: process.label,
|
||||
key: `${app.name}/${process.name}`,
|
||||
route: `${path}/:id/${process.name}`,
|
||||
component: <RecordView table={table} launchProcess={process} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const reportsForTable = ProcessUtils.getReportsForTable(metaData, table.name, true);
|
||||
@ -519,11 +487,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",
|
||||
@ -592,10 +559,7 @@ export default function App()
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// todo - this is auth0 logout... make more generic //
|
||||
//////////////////////////////////////////////////////
|
||||
logout();
|
||||
doLogout();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -603,7 +567,9 @@ export default function App()
|
||||
})();
|
||||
}, [needToLoadRoutes, isFullyAuthenticated]);
|
||||
|
||||
// Open sidenav when mouse enter on mini sidenav
|
||||
///////////////////////////////////////////////////
|
||||
// Open sidenav when mouse enter on mini sidenav //
|
||||
///////////////////////////////////////////////////
|
||||
const handleOnMouseEnter = () =>
|
||||
{
|
||||
if (miniSidenav && !onMouseEnter)
|
||||
@ -613,7 +579,9 @@ export default function App()
|
||||
}
|
||||
};
|
||||
|
||||
// Close sidenav when mouse leave mini sidenav
|
||||
/////////////////////////////////////////////////
|
||||
// Close sidenav when mouse leave mini sidenav //
|
||||
/////////////////////////////////////////////////
|
||||
const handleOnMouseLeave = () =>
|
||||
{
|
||||
if (onMouseEnter)
|
||||
@ -623,16 +591,14 @@ export default function App()
|
||||
}
|
||||
};
|
||||
|
||||
// Change the openConfigurator state
|
||||
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
|
||||
|
||||
// Setting the dir attribute for the body element
|
||||
useEffect(() =>
|
||||
{
|
||||
document.body.setAttribute("dir", direction);
|
||||
}, [direction]);
|
||||
|
||||
// Setting page scroll to 0 when changing the route
|
||||
//////////////////////////////////////////////////////
|
||||
// Setting page scroll to 0 when changing the route //
|
||||
//////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
document.documentElement.scrollTop = 0;
|
||||
@ -672,14 +638,14 @@ export default function App()
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
const [userId, setUserId] = useState(user?.email);
|
||||
const [userId, setUserId] = useState(loggedInUser?.email);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setUserId(user?.email)
|
||||
}, [user]);
|
||||
setUserId(loggedInUser?.email);
|
||||
}, [loggedInUser]);
|
||||
|
||||
|
||||
|
||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||
|
||||
/*******************************************************************************
|
||||
@ -687,9 +653,35 @@ export default function App()
|
||||
*******************************************************************************/
|
||||
function recordAnalytics(model: AnalyticsModel)
|
||||
{
|
||||
googleAnalyticsUtils.recordAnalytics(model)
|
||||
googleAnalyticsUtils.recordAnalytics(model);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if any of the auth/session setup code determined that we need //
|
||||
// to render something and return early - then do so here. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
if (earlyReturnForAuth)
|
||||
{
|
||||
return (earlyReturnForAuth);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function banner(): JSX.Element | null
|
||||
{
|
||||
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_SITE");
|
||||
|
||||
if (!banner)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", position: "sticky", top: "0", zIndex: 1, ...getBannerStyles(banner)}}>
|
||||
{makeBannerContent(banner)}
|
||||
</Box>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -718,6 +710,7 @@ export default function App()
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CommandMenu metaData={metaData} />
|
||||
{banner()}
|
||||
<Sidenav
|
||||
color={sidenavColor}
|
||||
icon={branding.icon}
|
||||
@ -727,6 +720,7 @@ export default function App()
|
||||
routes={sideNavRoutes}
|
||||
onMouseEnter={handleOnMouseEnter}
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
logout={doLogout}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
||||
|
150
src/index.tsx
150
src/index.tsx
@ -19,116 +19,104 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Auth0Provider} from "@auth0/auth0-react";
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import App from "App";
|
||||
import "qqq/styles/qqq-override-styles.css";
|
||||
import "qqq/styles/globals.scss";
|
||||
import "qqq/styles/raycast.scss";
|
||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// Expose React and ReactDOM as globals, for use by dynamically loaded modules //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
(window as any).React = React;
|
||||
(window as any).ReactDOM = ReactDOM;
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage()
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
}
|
||||
|
||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData()
|
||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData();
|
||||
|
||||
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
||||
{
|
||||
// @ts-ignore
|
||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// @ts-ignore
|
||||
const onRedirectCallback = (appState) =>
|
||||
{
|
||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||
};
|
||||
if (searchParams.get("error"))
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider {...props}>
|
||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||
{children}
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function Auth0RouterBody()
|
||||
{
|
||||
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
||||
return (renderAppWrapper(authenticationMetaData));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function OAuth2RouterBody()
|
||||
{
|
||||
const {renderAppWrapper} = useOAuth2AuthenticationModule({inOAuthContext: false});
|
||||
return (renderAppWrapper(authenticationMetaData, (
|
||||
<MaterialUIControllerProvider>
|
||||
<App authenticationMetaData={authenticationMetaData} />
|
||||
</MaterialUIControllerProvider>
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function AnonymousRouterBody()
|
||||
{
|
||||
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
||||
return (renderAppWrapper(authenticationMetaData, (
|
||||
<MaterialUIControllerProvider>
|
||||
<App authenticationMetaData={authenticationMetaData} />
|
||||
</MaterialUIControllerProvider>
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container);
|
||||
|
||||
if (authenticationMetaData.type === "AUTH_0")
|
||||
{
|
||||
// @ts-ignore
|
||||
let domain: string = authenticationMetaData.data.baseUrl;
|
||||
|
||||
// @ts-ignore
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
// @ts-ignore
|
||||
const audience = authenticationMetaData.data.audience;
|
||||
|
||||
if(!domain || !clientId)
|
||||
{
|
||||
root.render(
|
||||
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if(domain.endsWith("/"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
domain = domain.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<Auth0ProviderWithRedirectCallback
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
audience={audience}
|
||||
redirectUri={`${window.location.origin}/`}
|
||||
>
|
||||
<MaterialUIControllerProvider>
|
||||
<ProtectedRoute component={App} />
|
||||
</MaterialUIControllerProvider>
|
||||
</Auth0ProviderWithRedirectCallback>
|
||||
</BrowserRouter>
|
||||
);
|
||||
root.render(<BrowserRouter>
|
||||
<Auth0RouterBody />
|
||||
</BrowserRouter>);
|
||||
}
|
||||
else if (authenticationMetaData.type === "OAUTH2")
|
||||
{
|
||||
root.render(<BrowserRouter>
|
||||
<OAuth2RouterBody />
|
||||
</BrowserRouter>);
|
||||
}
|
||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||
{
|
||||
root.render(<BrowserRouter>
|
||||
<AnonymousRouterBody />
|
||||
</BrowserRouter>);
|
||||
}
|
||||
else
|
||||
{
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<MaterialUIControllerProvider>
|
||||
<App />
|
||||
</MaterialUIControllerProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
root.render(<div>
|
||||
Error: Unknown authenticationMetaData type: [{authenticationMetaData.type}].
|
||||
</div>);
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.actions.formadjuster;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class FormAdjusterInput
|
||||
{
|
||||
private String event;
|
||||
private String fieldName;
|
||||
|
||||
private Serializable newValue;
|
||||
private Map<String, Serializable> allValues;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for event
|
||||
*******************************************************************************/
|
||||
public String getEvent()
|
||||
{
|
||||
return (this.event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for event
|
||||
*******************************************************************************/
|
||||
public void setEvent(String event)
|
||||
{
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for event
|
||||
*******************************************************************************/
|
||||
public FormAdjusterInput withEvent(String event)
|
||||
{
|
||||
this.event = event;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public FormAdjusterInput withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for newValue
|
||||
*******************************************************************************/
|
||||
public Serializable getNewValue()
|
||||
{
|
||||
return (this.newValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for newValue
|
||||
*******************************************************************************/
|
||||
public void setNewValue(Serializable newValue)
|
||||
{
|
||||
this.newValue = newValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for newValue
|
||||
*******************************************************************************/
|
||||
public FormAdjusterInput withNewValue(Serializable newValue)
|
||||
{
|
||||
this.newValue = newValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for allValues
|
||||
*******************************************************************************/
|
||||
public Map<String, Serializable> getAllValues()
|
||||
{
|
||||
return (this.allValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for allValues
|
||||
*******************************************************************************/
|
||||
public void setAllValues(Map<String, Serializable> allValues)
|
||||
{
|
||||
this.allValues = allValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for allValues
|
||||
*******************************************************************************/
|
||||
public FormAdjusterInput withAllValues(Map<String, Serializable> allValues)
|
||||
{
|
||||
this.allValues = allValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.actions.formadjuster;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** interface to be implemented by application-specific form-adjusters
|
||||
*******************************************************************************/
|
||||
public interface FormAdjusterInterface
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
FormAdjusterOutput execute(FormAdjusterInput input) throws QException;
|
||||
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.actions.formadjuster;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class FormAdjusterOutput
|
||||
{
|
||||
private Map<String, QFrontendFieldMetaData> updatedFieldMetaData = null;
|
||||
private Map<String, Serializable> updatedFieldValues = null;
|
||||
private Map<String, String> updatedFieldDisplayValues = null;
|
||||
private Set<String> fieldsToClear = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFieldValues
|
||||
*******************************************************************************/
|
||||
public Map<String, Serializable> getUpdatedFieldValues()
|
||||
{
|
||||
return (this.updatedFieldValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFieldValues
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFieldValues(Map<String, Serializable> updatedFieldValues)
|
||||
{
|
||||
this.updatedFieldValues = updatedFieldValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFieldValues
|
||||
*******************************************************************************/
|
||||
public FormAdjusterOutput withUpdatedFieldValues(Map<String, Serializable> updatedFieldValues)
|
||||
{
|
||||
this.updatedFieldValues = updatedFieldValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldsToClear
|
||||
*******************************************************************************/
|
||||
public Set<String> getFieldsToClear()
|
||||
{
|
||||
return (this.fieldsToClear);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldsToClear
|
||||
*******************************************************************************/
|
||||
public void setFieldsToClear(Set<String> fieldsToClear)
|
||||
{
|
||||
this.fieldsToClear = fieldsToClear;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldsToClear
|
||||
*******************************************************************************/
|
||||
public FormAdjusterOutput withFieldsToClear(Set<String> fieldsToClear)
|
||||
{
|
||||
this.fieldsToClear = fieldsToClear;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFieldMetaData
|
||||
*******************************************************************************/
|
||||
public Map<String, QFrontendFieldMetaData> getUpdatedFieldMetaData()
|
||||
{
|
||||
return (this.updatedFieldMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFieldMetaData
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFieldMetaData(Map<String, QFrontendFieldMetaData> updatedFieldMetaData)
|
||||
{
|
||||
this.updatedFieldMetaData = updatedFieldMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFieldMetaData
|
||||
*******************************************************************************/
|
||||
public FormAdjusterOutput withUpdatedFieldMetaData(Map<String, QFrontendFieldMetaData> updatedFieldMetaData)
|
||||
{
|
||||
this.updatedFieldMetaData = updatedFieldMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFieldDisplayValues
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getUpdatedFieldDisplayValues()
|
||||
{
|
||||
return (this.updatedFieldDisplayValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFieldDisplayValues
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFieldDisplayValues(Map<String, String> updatedFieldDisplayValues)
|
||||
{
|
||||
this.updatedFieldDisplayValues = updatedFieldDisplayValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFieldDisplayValues
|
||||
*******************************************************************************/
|
||||
public FormAdjusterOutput withUpdatedFieldDisplayValues(Map<String, String> updatedFieldDisplayValues)
|
||||
{
|
||||
this.updatedFieldDisplayValues = updatedFieldDisplayValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.actions.formadjuster;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
|
||||
import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.model.metadata.MaterialDashboardFieldMetaData;
|
||||
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Class that stores code-references for the application's defined fromAdjusters
|
||||
** This class also, when registering its first formAdjuster, adds the route to
|
||||
** the javalin instance to service form-adjuster calls from the frontend.
|
||||
*******************************************************************************/
|
||||
public class FormAdjusterRegistry
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(FormAdjusterRegistry.class);
|
||||
|
||||
private static boolean didRegisterRouteProvider = false;
|
||||
private static QInstance lastRegisteredQInstance = null;
|
||||
|
||||
private static Map<String, QCodeReference> onChangeAdjusters = new HashMap<>();
|
||||
private static Map<String, QCodeReference> onLoadAdjusters = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void registerFormAdjusters(QInstance qInstance, MaterialDashboardFieldMetaData materialDashboardFieldMetaData) throws QException
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// support hot-swaps, by checking if the input qInstance is different from one we previously registered for //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(didRegisterRouteProvider && lastRegisteredQInstance != qInstance)
|
||||
{
|
||||
didRegisterRouteProvider = false;
|
||||
onChangeAdjusters.clear();
|
||||
onLoadAdjusters.clear();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we need to register the javalin router, do so (only once per qInstance) //
|
||||
// note, javalin is optional dep, so make sure it's available before try to use it //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!didRegisterRouteProvider)
|
||||
{
|
||||
if(ClassPathUtils.isClassAvailable(QJavalinMetaData.class.getName()))
|
||||
{
|
||||
QJavalinMetaData javalinMetaData = QJavalinMetaData.ofOrWithNew(qInstance);
|
||||
javalinMetaData.withRouteProvider(new JavalinRouteProviderMetaData()
|
||||
.withHostedPath("/material-dashboard-backend/form-adjuster/{identifier}/{event}")
|
||||
.withMethods(List.of("POST"))
|
||||
.withProcessName(RunFormAdjusterProcess.NAME)
|
||||
);
|
||||
|
||||
qInstance.add(new RunFormAdjusterProcess().produce(qInstance));
|
||||
}
|
||||
|
||||
didRegisterRouteProvider = true;
|
||||
lastRegisteredQInstance = qInstance;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// add the code-references to the map of registered adjusters //
|
||||
////////////////////////////////////////////////////////////////
|
||||
String identifier = materialDashboardFieldMetaData.getFormAdjusterIdentifier();
|
||||
|
||||
QCodeReference onChangeCode = materialDashboardFieldMetaData.getOnChangeFormAdjuster();
|
||||
if(onChangeCode != null)
|
||||
{
|
||||
if(onChangeAdjusters.containsKey(identifier))
|
||||
{
|
||||
LOG.warn("Attempt to register more than one onChangeFormAdjuster with identifier: " + identifier);
|
||||
}
|
||||
onChangeAdjusters.put(identifier, onChangeCode);
|
||||
}
|
||||
|
||||
QCodeReference onLoadCode = materialDashboardFieldMetaData.getOnLoadFormAdjuster();
|
||||
if(onLoadCode != null)
|
||||
{
|
||||
if(onLoadAdjusters.containsKey(identifier))
|
||||
{
|
||||
LOG.warn("Attempt to register more than one onLoadFormAdjuster with identifier: " + identifier);
|
||||
}
|
||||
onLoadAdjusters.put(identifier, onLoadCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static FormAdjusterInterface getOnChangeAdjuster(String identifier)
|
||||
{
|
||||
QCodeReference codeReference = onChangeAdjusters.get(identifier);
|
||||
if(codeReference != null)
|
||||
{
|
||||
return QCodeLoader.getAdHoc(FormAdjusterInterface.class, codeReference);
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
static FormAdjusterInterface getOnLoadAdjuster(String identifier)
|
||||
{
|
||||
QCodeReference codeReference = onLoadAdjusters.get(identifier);
|
||||
if(codeReference != null)
|
||||
{
|
||||
return QCodeLoader.getAdHoc(FormAdjusterInterface.class, codeReference);
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.actions.formadjuster;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** process that looks up a form adjuster from the registry, and then runs it
|
||||
*******************************************************************************/
|
||||
public class RunFormAdjusterProcess implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "MaterialDashboardRunFormAdjusterProcess";
|
||||
|
||||
private static final QLogger LOG = QLogger.getLogger(RunFormAdjusterProcess.class);
|
||||
|
||||
public static final String EVENT_ON_LOAD = "onLoad";
|
||||
public static final String EVENT_ON_CHANGE = "onChange";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
|
||||
.withStep(new QBackendStepMetaData()
|
||||
.withName("execute")
|
||||
.withCode(new QCodeReference(getClass())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
ProcessBasedRouterPayload payload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class);
|
||||
|
||||
String identifier = payload.getPathParams().get("identifier");
|
||||
String event = payload.getPathParams().get("event");
|
||||
|
||||
try
|
||||
{
|
||||
FormAdjusterInterface formAdjuster = switch(event)
|
||||
{
|
||||
case EVENT_ON_CHANGE -> FormAdjusterRegistry.getOnChangeAdjuster(identifier);
|
||||
case EVENT_ON_LOAD -> FormAdjusterRegistry.getOnLoadAdjuster(identifier);
|
||||
default -> throw new QException("Unknown event type: " + event);
|
||||
};
|
||||
|
||||
if(formAdjuster == null)
|
||||
{
|
||||
throw new QException("No form adjuster found for identifier: " + identifier + " and event: " + event);
|
||||
}
|
||||
|
||||
FormAdjusterInput input = new FormAdjusterInput();
|
||||
input.setEvent(event);
|
||||
input.setFieldName(payload.getFormParam("fieldName"));
|
||||
input.setNewValue(payload.getFormParam("newValue"));
|
||||
|
||||
String allValuesJson = payload.getFormParam("allValues");
|
||||
Map<String, Serializable> allValues = StringUtils.hasContent(allValuesJson) ? JsonUtils.toObject(allValuesJson, new TypeReference<>() {}) : Collections.emptyMap();
|
||||
input.setAllValues(allValues);
|
||||
|
||||
FormAdjusterOutput output = formAdjuster.execute(input);
|
||||
|
||||
payload.setResponseString(JsonUtils.toJson(output));
|
||||
runBackendStepOutput.setProcessPayload(payload);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error running form adjuster process", e, logPair("identifier", identifier), logPair("event", event));
|
||||
throw new QException("Error running form adjuster process: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,20 +19,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useAuth0} from "@auth0/auth0-react";
|
||||
import {Button} from "@mui/material";
|
||||
import React from "react";
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||
|
||||
function AuthenticationButton()
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum MaterialDashboardBannerSlots implements BannerSlot
|
||||
{
|
||||
const {loginWithRedirect, logout, isAuthenticated} = useAuth0();
|
||||
|
||||
if (isAuthenticated)
|
||||
{
|
||||
return <Button onClick={() => logout({returnTo: window.location.origin})}>Log Out</Button>;
|
||||
}
|
||||
|
||||
return <Button onClick={() => loginWithRedirect()}>Log In</Button>;
|
||||
QFMD_TOP_OF_SITE,
|
||||
QFMD_TOP_OF_BODY,
|
||||
QFMD_SIDE_NAV_UNDER_LOGO
|
||||
}
|
||||
|
||||
export default AuthenticationButton;
|
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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 java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QSupplementalFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterInterface;
|
||||
import com.kingsrook.qqq.frontend.materialdashboard.actions.formadjuster.FormAdjusterRegistry;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class MaterialDashboardFieldMetaData extends QSupplementalFieldMetaData
|
||||
{
|
||||
public static final String TYPE = "materialDashboard";
|
||||
|
||||
private static final QLogger LOG = QLogger.getLogger(MaterialDashboardFieldMetaData.class);
|
||||
|
||||
private String formAdjusterIdentifier = null;
|
||||
private QCodeReference onChangeFormAdjuster = null;
|
||||
private QCodeReference onLoadFormAdjuster = null;
|
||||
private Set<String> fieldsToDisableWhileRunningAdjusters = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean includeInFrontendMetaData()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for onChangeFormAdjuster
|
||||
*******************************************************************************/
|
||||
public QCodeReference getOnChangeFormAdjuster()
|
||||
{
|
||||
return (this.onChangeFormAdjuster);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for onChangeFormAdjuster
|
||||
*******************************************************************************/
|
||||
public void setOnChangeFormAdjuster(QCodeReference onChangeFormAdjuster)
|
||||
{
|
||||
this.onChangeFormAdjuster = onChangeFormAdjuster;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for onChangeFormAdjuster
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardFieldMetaData withOnChangeFormAdjuster(QCodeReference onChangeFormAdjuster)
|
||||
{
|
||||
this.onChangeFormAdjuster = onChangeFormAdjuster;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for onLoadFormAdjuster
|
||||
*******************************************************************************/
|
||||
public QCodeReference getOnLoadFormAdjuster()
|
||||
{
|
||||
return (this.onLoadFormAdjuster);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for onLoadFormAdjuster
|
||||
*******************************************************************************/
|
||||
public void setOnLoadFormAdjuster(QCodeReference onLoadFormAdjuster)
|
||||
{
|
||||
this.onLoadFormAdjuster = onLoadFormAdjuster;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for onLoadFormAdjuster
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardFieldMetaData withOnLoadFormAdjuster(QCodeReference onLoadFormAdjuster)
|
||||
{
|
||||
this.onLoadFormAdjuster = onLoadFormAdjuster;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void enrich(QInstance qInstance, QFieldMetaData fieldMetaData)
|
||||
{
|
||||
try
|
||||
{
|
||||
FormAdjusterRegistry.registerFormAdjusters(qInstance, this);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error enriching MaterialDashboardFieldMetaData", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, QFieldMetaData fieldMetaData, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
String prefix = "MaterialDashboardFieldMetaData for field [" + fieldMetaData.getName() + "]";
|
||||
|
||||
boolean needsFormAdjusterIdentifer = false;
|
||||
if(onChangeFormAdjuster != null)
|
||||
{
|
||||
needsFormAdjusterIdentifer = true;
|
||||
qInstanceValidator.validateSimpleCodeReference(prefix + ", onChangeFormAdjuster", onChangeFormAdjuster, FormAdjusterInterface.class);
|
||||
}
|
||||
|
||||
if(onLoadFormAdjuster != null)
|
||||
{
|
||||
needsFormAdjusterIdentifer = true;
|
||||
qInstanceValidator.validateSimpleCodeReference(prefix + ", onLoadFormAdjuster", onLoadFormAdjuster, FormAdjusterInterface.class);
|
||||
}
|
||||
|
||||
if(needsFormAdjusterIdentifer)
|
||||
{
|
||||
qInstanceValidator.assertCondition(StringUtils.hasContent(formAdjusterIdentifier), prefix + ", formAdjusterIdentifier is required if using any FormAdjusters");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for formAdjusterIdentifier
|
||||
*******************************************************************************/
|
||||
public String getFormAdjusterIdentifier()
|
||||
{
|
||||
return (this.formAdjusterIdentifier);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for formAdjusterIdentifier
|
||||
*******************************************************************************/
|
||||
public void setFormAdjusterIdentifier(String formAdjusterIdentifier)
|
||||
{
|
||||
this.formAdjusterIdentifier = formAdjusterIdentifier;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for formAdjusterIdentifier
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardFieldMetaData withFormAdjusterIdentifier(String formAdjusterIdentifier)
|
||||
{
|
||||
this.formAdjusterIdentifier = formAdjusterIdentifier;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldsToDisableWhileRunningAdjusters
|
||||
*******************************************************************************/
|
||||
public Set<String> getFieldsToDisableWhileRunningAdjusters()
|
||||
{
|
||||
return (this.fieldsToDisableWhileRunningAdjusters);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldsToDisableWhileRunningAdjusters
|
||||
*******************************************************************************/
|
||||
public void setFieldsToDisableWhileRunningAdjusters(Set<String> fieldsToDisableWhileRunningAdjusters)
|
||||
{
|
||||
this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldsToDisableWhileRunningAdjusters
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardFieldMetaData withFieldsToDisableWhileRunningAdjusters(Set<String> fieldsToDisableWhileRunningAdjusters)
|
||||
{
|
||||
this.fieldsToDisableWhileRunningAdjusters = fieldsToDisableWhileRunningAdjusters;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** table-level meta-data for this module (handled as QSupplementalTableMetaData)
|
||||
*******************************************************************************/
|
||||
public class MaterialDashboardInstanceMetaData implements QSupplementalInstanceMetaData
|
||||
{
|
||||
public static final String TYPE = "materialDashboard";
|
||||
|
||||
private List<String> processNamesToAddToAllQueryAndViewScreens = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return (TYPE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static MaterialDashboardInstanceMetaData ofOrWithNew(QInstance qInstance)
|
||||
{
|
||||
MaterialDashboardInstanceMetaData supplementalMetaData = (MaterialDashboardInstanceMetaData) qInstance.getSupplementalMetaData(TYPE);
|
||||
if(supplementalMetaData == null)
|
||||
{
|
||||
supplementalMetaData = new MaterialDashboardInstanceMetaData();
|
||||
qInstance.withSupplementalMetaData(supplementalMetaData);
|
||||
}
|
||||
|
||||
return (supplementalMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for processNamesToAddToAllQueryAndViewScreens
|
||||
*******************************************************************************/
|
||||
public List<String> getProcessNamesToAddToAllQueryAndViewScreens()
|
||||
{
|
||||
return (this.processNamesToAddToAllQueryAndViewScreens);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addProcessNameToAddToAllQueryAndViewScreens(String processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
if(this.processNamesToAddToAllQueryAndViewScreens == null)
|
||||
{
|
||||
this.processNamesToAddToAllQueryAndViewScreens = new ArrayList<>();
|
||||
}
|
||||
this.processNamesToAddToAllQueryAndViewScreens.add(processNamesToAddToAllQueryAndViewScreens);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for processNamesToAddToAllQueryAndViewScreens
|
||||
*******************************************************************************/
|
||||
public void setProcessNamesToAddToAllQueryAndViewScreens(List<String> processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for processNamesToAddToAllQueryAndViewScreens
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardInstanceMetaData withProcessNamesToAddToAllQueryAndViewScreens(List<String> processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
this.processNamesToAddToAllQueryAndViewScreens = processNamesToAddToAllQueryAndViewScreens;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {Md5} from "ts-md5/dist/md5";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the anonymous authentication module
|
||||
***************************************************************************/
|
||||
export default function useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||
{
|
||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
console.log("Generating random token...");
|
||||
setIsFullyAuthenticated(true);
|
||||
Client.setGotAuthenticationInAllControllers();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||
{
|
||||
return children;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
253
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
253
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Auth0Provider, useAuth0} from "@auth0/auth0-react";
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import App, {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
const qControllerV1 = Client.getInstanceV1();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the Auth0 authentication module
|
||||
***************************************************************************/
|
||||
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser}: Props)
|
||||
{
|
||||
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
||||
|
||||
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||
{
|
||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||
{
|
||||
console.log("No session uuid cookie - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
if (!oldToken)
|
||||
{
|
||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const oldJSON: any = jwt_decode(oldToken);
|
||||
const newJSON: any = jwt_decode(newToken);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the old (local storage) token is expired, then we need to store the new one //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
const oldExp = oldJSON["exp"];
|
||||
if (oldExp * 1000 < (new Date().getTime()))
|
||||
{
|
||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
delete newJSON["exp"];
|
||||
delete newJSON["iat"];
|
||||
delete oldJSON["exp"];
|
||||
delete oldJSON["iat"];
|
||||
|
||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||
if (different)
|
||||
{
|
||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||
}
|
||||
return (different);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log("Caught in shouldStoreNewToken: " + e);
|
||||
}
|
||||
|
||||
return (true);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
console.log("Loading token from auth0...");
|
||||
const accessToken = await auth0GetAccessTokenSilently();
|
||||
|
||||
const lsAccessToken = localStorage.getItem("accessToken");
|
||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||
{
|
||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||
const {uuid: values} = await qController.manageSession(accessToken, null);
|
||||
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
Client.setGotAuthenticationInAllControllers();
|
||||
|
||||
setLoggedInUser(auth0User);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
localStorage.removeItem("accessToken");
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
useAuth0Logout();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
useAuth0Logout({returnTo: window.location.origin});
|
||||
};
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
// @ts-ignore
|
||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// @ts-ignore
|
||||
const onRedirectCallback = (appState) =>
|
||||
{
|
||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||
};
|
||||
if (searchParams.get("error"))
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider {...props}>
|
||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||
{children}
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData): JSX.Element =>
|
||||
{
|
||||
// @ts-ignore
|
||||
let domain: string = authenticationMetaData.data.baseUrl;
|
||||
|
||||
// @ts-ignore
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
// @ts-ignore
|
||||
const audience = authenticationMetaData.data.audience;
|
||||
|
||||
if (!domain || !clientId)
|
||||
{
|
||||
return (
|
||||
<div>Error: AUTH0 authenticationMetaData is missing baseUrl [{domain}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (domain.endsWith("/"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
domain = domain.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** simple Functional Component to wrap the <App> and pass the authentication-
|
||||
** MetaData prop in, so a simple Component can be passed into ProtectedRoute
|
||||
***************************************************************************/
|
||||
function WrappedApp()
|
||||
{
|
||||
return <App authenticationMetaData={authenticationMetaData} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Auth0ProviderWithRedirectCallback
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
audience={audience}
|
||||
redirectUri={`${window.location.origin}/`}>
|
||||
<MaterialUIControllerProvider>
|
||||
<ProtectedRoute component={WrappedApp} />
|
||||
</MaterialUIControllerProvider>
|
||||
</Auth0ProviderWithRedirectCallback>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
188
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
188
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {AuthContextProps, AuthProvider, useAuth} from "react-oidc-context";
|
||||
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||
setLoggedInUser?: (user: any) => void;
|
||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||
inOAuthContext: boolean;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** hook for working with the OAuth2 authentication module
|
||||
***************************************************************************/
|
||||
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext}: Props)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// the useAuth hook should only be called if we're inside the <AuthProvider> element //
|
||||
// so on the page that uses this hook to call renderAppWrapper, we aren't in that //
|
||||
// element/context, thus, don't call that hook. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
const authOidc: AuthContextProps | null = inOAuthContext ? useAuth() : null;
|
||||
|
||||
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const setupSession = async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
||||
if (window.location.pathname == "/token")
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if we're at a path of /token, get code & state params, look up values //
|
||||
// from that state in local storage, and make a post to the backend to //
|
||||
// with these values - which will itself talk to the identity provider //
|
||||
// to get an access token, and ultimately a session. //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
const code = searchParams.get("code");
|
||||
const state = searchParams.get("state");
|
||||
const oidcString = localStorage.getItem(`oidc.${state}`);
|
||||
if (oidcString)
|
||||
{
|
||||
const oidcObject = JSON.parse(oidcString) as { [name: string]: any };
|
||||
console.log(oidcObject);
|
||||
const manageSessionRequestBody = {code: code, codeVerifier: oidcObject.code_verifier, redirectUri: oidcObject.redirect_uri};
|
||||
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
|
||||
console.log(`we have new session UUID: ${newSessionUuid}`);
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
Client.setGotAuthenticationInAllControllers();
|
||||
|
||||
setLoggedInUser(values?.user);
|
||||
console.log("Token load complete.");
|
||||
|
||||
const preSigninRedirectPathname = localStorage.getItem(preSigninRedirectPathnameKey);
|
||||
localStorage.removeItem(preSigninRedirectPathname);
|
||||
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// if unrecognized state, render an error //
|
||||
////////////////////////////////////////////
|
||||
setEarlyReturnForAuth(<div>Login error: Unrecognized state. Refresh to try again.</div>);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if we have a sessionUUID cookie, try to validate it with the backend //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
||||
if (sessionUuid)
|
||||
{
|
||||
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
||||
const {values} = await qController.manageSession(null, sessionUuid, null);
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
Client.setGotAuthenticationInAllControllers();
|
||||
|
||||
setLoggedInUser(values?.user);
|
||||
console.log("Token load complete.");
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else no cookie, and not a token url, we need to redirect to the provider's login page //
|
||||
// capture the path the user was trying to access in local storage, to redirect back to later. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
console.log("Loading token from OAuth2 provider...");
|
||||
console.log(authOidc);
|
||||
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||
authOidc?.signinRedirect();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const logout = () =>
|
||||
{
|
||||
qController.clearAuthenticationMetaDataLocalStorage();
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
authOidc?.signoutRedirect();
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||
{
|
||||
const authority: string = authenticationMetaData.data.baseUrl;
|
||||
const clientId = authenticationMetaData.data.clientId;
|
||||
|
||||
if (!authority || !clientId)
|
||||
{
|
||||
return (
|
||||
<div>Error: OAuth2 authenticationMetaData is missing baseUrl [{authority}] and/or clientId [{clientId}].</div>
|
||||
);
|
||||
}
|
||||
|
||||
const oidcConfig =
|
||||
{
|
||||
authority: authority,
|
||||
client_id: clientId,
|
||||
redirect_uri: `${window.location.origin}/token`,
|
||||
response_type: "code",
|
||||
scope: "openid profile email offline_access",
|
||||
};
|
||||
|
||||
return (<AuthProvider {...oidcConfig}>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
setupSession,
|
||||
logout,
|
||||
renderAppWrapper
|
||||
};
|
||||
|
||||
}
|
@ -23,8 +23,10 @@ import {Chip} from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {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,107 @@ 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();
|
||||
|
||||
if (chipType !== "pvs")
|
||||
{
|
||||
const currentChipValidity = chips.map((chip, i) =>
|
||||
(chipType !== "number" || !Number.isNaN(Number(chips[i])))
|
||||
);
|
||||
setChipValidity(currentChipValidity);
|
||||
}
|
||||
}, [JSON.stringify(chipData), chips]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
handleChipChange(chips);
|
||||
}, [chips, handleChipChange]);
|
||||
handleChipChange(isMakingRequest, chipValidity, chipPVSIds);
|
||||
}, [chipValidity, chipPVSIds, isMakingRequest]);
|
||||
|
||||
function handleKeyDown(event: any)
|
||||
{
|
||||
@ -64,13 +152,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 +178,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 +215,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 +224,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 +257,7 @@ function ChipTextField({...props})
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ChipTextField.defaultProps = {
|
||||
chipData: []
|
||||
};
|
||||
@ -166,4 +266,4 @@ ChipTextField.propTypes = {
|
||||
chipData: arrayOf(string)
|
||||
};
|
||||
|
||||
export default ChipTextField
|
||||
export default ChipTextField;
|
||||
|
@ -20,15 +20,21 @@
|
||||
*/
|
||||
|
||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import {useFormikContext} from "formik";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||
import FileInputField from "qqq/components/forms/FileInputField";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import React from "react";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -43,7 +49,12 @@ interface Props
|
||||
|
||||
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
|
||||
{
|
||||
const {formFields, values, errors, touched} = formData;
|
||||
const {formFields: origFormFields, errors, touched} = formData;
|
||||
const {setFieldValue, values} = useFormikContext<Record<string, any>>();
|
||||
|
||||
const [formAdjustmentCounter, setFormAdjustmentCounter] = useState(0)
|
||||
|
||||
const [formFields, setFormFields] = useState(origFormFields as {[key: string]: any});
|
||||
|
||||
const bulkEditSwitchChanged = (name: string, value: boolean) =>
|
||||
{
|
||||
@ -51,13 +62,211 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
};
|
||||
|
||||
|
||||
/////////////////////////////////////////
|
||||
// run on-load handlers if we have any //
|
||||
/////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
for (let fieldName in formFields)
|
||||
{
|
||||
const field = formFields[fieldName];
|
||||
|
||||
const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
|
||||
if(materialDashboardFieldMetaData?.onLoadFormAdjuster)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo consider cases with multiple - do they need to list a sequenceNo? do they need to run serially? //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
considerRunningFormAdjuster("onLoad", fieldName, values[fieldName]);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const handleFieldChange = async (fieldName: string, newValue: any) =>
|
||||
{
|
||||
const field = formFields[fieldName];
|
||||
if (!field)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// map possible-value objects to ids - also capture their labels... //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
let actualNewValue = newValue;
|
||||
let possibleValueLabel: string = null;
|
||||
if (field.possibleValueProps)
|
||||
{
|
||||
actualNewValue = newValue ? newValue.id : null;
|
||||
possibleValueLabel = newValue ? newValue.label : null;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure formik has the value - and that we capture the possible-value label if needed //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
setFieldValue(fieldName, actualNewValue);
|
||||
if (field.possibleValueProps)
|
||||
{
|
||||
field.possibleValueProps.initialDisplayValue = possibleValueLabel;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// run onChange adjuster if there is one //
|
||||
///////////////////////////////////////////
|
||||
considerRunningFormAdjuster("onChange", fieldName, actualNewValue);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const considerRunningFormAdjuster = async (event: "onChange" | "onLoad", fieldName: string, newValue: any) =>
|
||||
{
|
||||
const field = formFields[fieldName];
|
||||
if (!field)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const materialDashboardFieldMetaData = field.fieldMetaData?.supplementalFieldMetaData?.get("materialDashboard");
|
||||
const adjuster = event == "onChange" ? materialDashboardFieldMetaData?.onChangeFormAdjuster : materialDashboardFieldMetaData?.onLoadFormAdjuster;
|
||||
if (!adjuster)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Running form adjuster for field ${fieldName} ${event} (value is: ${newValue})`);
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// disable fields temporarily while waiting on backend response //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
const fieldNamesToTempDisable: string[] = materialDashboardFieldMetaData?.fieldsToDisableWhileRunningAdjusters ?? []
|
||||
const previousIsEditableValues: {[key: string]: boolean} = {};
|
||||
if(fieldNamesToTempDisable.length > 0)
|
||||
{
|
||||
for (let oldFieldName in formFields)
|
||||
{
|
||||
if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
|
||||
{
|
||||
previousIsEditableValues[oldFieldName] = formFields[oldFieldName].isEditable;
|
||||
formFields[oldFieldName].isEditable = false;
|
||||
}
|
||||
}
|
||||
|
||||
setFormAdjustmentCounter(formAdjustmentCounter + 1);
|
||||
setFormFields({...formFields});
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// build request to backend for field adjustments //
|
||||
////////////////////////////////////////////////////
|
||||
const postBody = new FormData();
|
||||
postBody.append("event", event);
|
||||
postBody.append("fieldName", fieldName);
|
||||
postBody.append("newValue", newValue);
|
||||
postBody.append("allValues", JSON.stringify(values));
|
||||
const response = await qController.axiosRequest(
|
||||
{
|
||||
method: "post",
|
||||
url: `/material-dashboard-backend/form-adjuster/${encodeURIComponent(materialDashboardFieldMetaData.formAdjusterIdentifier)}/${event}`,
|
||||
data: postBody,
|
||||
headers: qController.defaultMultipartFormDataHeaders()
|
||||
});
|
||||
console.log("Form adjuster response: " + JSON.stringify(response));
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// un-disable any temp disabled fields from above //
|
||||
////////////////////////////////////////////////////
|
||||
if(fieldNamesToTempDisable.length > 0)
|
||||
{
|
||||
for (let oldFieldName in formFields)
|
||||
{
|
||||
if (fieldNamesToTempDisable.indexOf(oldFieldName) > -1)
|
||||
{
|
||||
formFields[oldFieldName].isEditable = previousIsEditableValues[oldFieldName];
|
||||
}
|
||||
}
|
||||
setFormFields({...formFields});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// replace field definitions, if we have updates //
|
||||
///////////////////////////////////////////////////
|
||||
const updatedFields: { [fieldName: string]: QFieldMetaData } = response.updatedFieldMetaData;
|
||||
if(updatedFields)
|
||||
{
|
||||
for (let updatedFieldName in updatedFields)
|
||||
{
|
||||
const updatedField = new QFieldMetaData(updatedFields[updatedFieldName]);
|
||||
const dynamicField = DynamicFormUtils.getDynamicField(updatedField); // todo dynamicallyDisabledFields? second param...
|
||||
|
||||
const dynamicFieldInObject: any = {};
|
||||
dynamicFieldInObject[updatedFieldName] = dynamicField;
|
||||
let tableName = null;
|
||||
let processName = null;
|
||||
let displayValues = new Map();
|
||||
|
||||
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [updatedFields[updatedFieldName]], tableName, processName, displayValues);
|
||||
for (let oldFieldName in formFields)
|
||||
{
|
||||
if (oldFieldName == updatedFieldName)
|
||||
{
|
||||
formFields[updatedFieldName] = dynamicField;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFormAdjustmentCounter(formAdjustmentCounter + 2);
|
||||
setFormFields({...formFields});
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// update field values //
|
||||
/////////////////////////
|
||||
const updatedFieldValues: {[fieldName: string]: any} = response?.updatedFieldValues ?? {};
|
||||
for (let fieldNameToUpdate in updatedFieldValues)
|
||||
{
|
||||
setFieldValue(fieldNameToUpdate, updatedFieldValues[fieldNameToUpdate]);
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - track if a pvs field gets a value, but not a display value, and fetch it?? //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// set display values in PVS's if we have them //
|
||||
/////////////////////////////////////////////////
|
||||
const updatedFieldDisplayValues: {[fieldName: string]: any} = response?.updatedFieldDisplayValues ?? {};
|
||||
for (let fieldNameToUpdate in updatedFieldDisplayValues)
|
||||
{
|
||||
const fieldToUpdate = formFields[fieldNameToUpdate];
|
||||
if(fieldToUpdate?.possibleValueProps)
|
||||
{
|
||||
fieldToUpdate.possibleValueProps.initialDisplayValue = updatedFieldDisplayValues[fieldNameToUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// clear field values if we have them //
|
||||
////////////////////////////////////////
|
||||
const fieldsToClear: string[] = response?.fieldsToClear ?? [];
|
||||
for (let fieldToClear of fieldsToClear)
|
||||
{
|
||||
setFieldValue(fieldToClear, "");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box lineHeight={0}>
|
||||
<MDTypography variant="h5">{formLabel}</MDTypography>
|
||||
</Box>
|
||||
<Box mt={1.625}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid container lg={12} display="flex" spacing={3}>
|
||||
{formFields
|
||||
&& Object.keys(formFields).length > 0
|
||||
&& Object.keys(formFields).map((fieldName: any) =>
|
||||
@ -68,19 +277,22 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
return null;
|
||||
}
|
||||
|
||||
const display = field.fieldMetaData?.isHidden ? "none" : "initial";
|
||||
|
||||
if (values[fieldName] === undefined)
|
||||
{
|
||||
values[fieldName] = "";
|
||||
}
|
||||
|
||||
let formattedHelpContent = <HelpContent helpContents={field?.fieldMetaData?.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
|
||||
if(formattedHelpContent)
|
||||
if (formattedHelpContent)
|
||||
{
|
||||
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>
|
||||
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>;
|
||||
}
|
||||
|
||||
const labelElement = <DynamicFormFieldLabel name={field.name} label={field.label} />;
|
||||
|
||||
let itemLG = (field?.fieldMetaData?.gridColumns && field?.fieldMetaData?.gridColumns > 0) ? field.fieldMetaData.gridColumns : 6;
|
||||
let itemXS = 12;
|
||||
let itemSM = 6;
|
||||
|
||||
@ -92,13 +304,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
const fileUploadAdornment = field.fieldMetaData?.getAdornment(AdornmentType.FILE_UPLOAD);
|
||||
const width = fileUploadAdornment?.values?.get("width") ?? "half";
|
||||
|
||||
if(width == "full")
|
||||
if (width == "full")
|
||||
{
|
||||
itemSM = 12;
|
||||
itemLG = 12;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
||||
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} flexDirection="column" key={fieldName + "-" + formAdjustmentCounter}>
|
||||
{labelElement}
|
||||
<FileInputField field={field} record={record} errorMessage={errors[fieldName]} />
|
||||
</Grid>
|
||||
@ -114,10 +327,10 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
Object.keys(values).forEach((key) =>
|
||||
{
|
||||
otherValuesMap.set(key, values[key]);
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
||||
<Grid item display={display} lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName + "-" + formAdjustmentCounter}>
|
||||
{labelElement}
|
||||
<DynamicSelect
|
||||
fieldPossibleValueProps={field.possibleValueProps}
|
||||
@ -128,6 +341,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
useCase="form"
|
||||
onChange={(newValue: any) => handleFieldChange(fieldName, newValue)}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
@ -138,7 +352,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
// everything else!! //
|
||||
///////////////////////
|
||||
return (
|
||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
||||
<Grid item display={display} lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName + "-" + formAdjustmentCounter}>
|
||||
{labelElement}
|
||||
<QDynamicFormField
|
||||
id={field.name}
|
||||
@ -153,6 +367,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
||||
formFieldObject={field}
|
||||
onChangeCallback={(newValue) => handleFieldChange(fieldName, newValue)}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
|
@ -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,10 @@ function QDynamicFormField({
|
||||
newValue = newValue.toLowerCase();
|
||||
}
|
||||
setFieldValue(name, newValue);
|
||||
onChangeCallback(newValue);
|
||||
if (onChangeCallback)
|
||||
{
|
||||
onChangeCallback(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
const input = document.getElementById(name) as HTMLInputElement;
|
||||
@ -150,7 +154,7 @@ function QDynamicFormField({
|
||||
input.setSelectionRange(beforeStart, beforeEnd);
|
||||
}
|
||||
}
|
||||
else if(onChangeCallback)
|
||||
else if (onChangeCallback)
|
||||
{
|
||||
onChangeCallback(e.currentTarget.value);
|
||||
}
|
||||
@ -162,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}
|
||||
@ -183,7 +187,7 @@ function QDynamicFormField({
|
||||
onChange={dynamicSelectOnChange}
|
||||
// otherValues={otherValuesMap}
|
||||
useCase="form"
|
||||
/>)
|
||||
/>);
|
||||
}
|
||||
else if (type === "checkbox")
|
||||
{
|
||||
@ -217,7 +221,7 @@ function QDynamicFormField({
|
||||
onChange={(value: string, event: any) =>
|
||||
{
|
||||
setFieldValue(name, value, false);
|
||||
if(onChangeCallback)
|
||||
if (onChangeCallback)
|
||||
{
|
||||
onChangeCallback(value);
|
||||
}
|
||||
|
@ -191,6 +191,11 @@ class DynamicFormUtils
|
||||
props.possibleValueSourceName = field.possibleValueSourceName;
|
||||
}
|
||||
|
||||
if(field.possibleValueSourceFilter)
|
||||
{
|
||||
props.possibleValueSourceFilter = field.possibleValueSourceFilter;
|
||||
}
|
||||
|
||||
dynamicFormFields[field.name].possibleValueProps = props;
|
||||
}
|
||||
}
|
||||
|
@ -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,24 @@ 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,
|
||||
fieldNameOrPossibleValueSourceName: possibleValueSourceName ?? fieldName,
|
||||
searchTerm: searchTerm ?? "",
|
||||
values: otherValues,
|
||||
useCase,
|
||||
possibleValueSourceFilter: fieldPossibleValueProps.possibleValueSourceFilter
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
|
@ -59,15 +59,17 @@ import * as Yup from "yup";
|
||||
|
||||
interface Props
|
||||
{
|
||||
id?: string;
|
||||
isModal: boolean;
|
||||
table?: QTableMetaData;
|
||||
closeModalHandler?: (event: object, reason: string) => void;
|
||||
defaultValues: { [key: string]: string };
|
||||
disabledFields: { [key: string]: boolean } | string[];
|
||||
isCopy?: boolean;
|
||||
onSubmitCallback?: (values: any) => void;
|
||||
overrideHeading?: string;
|
||||
id?: string,
|
||||
isModal: boolean,
|
||||
table?: QTableMetaData,
|
||||
closeModalHandler?: (event: object, reason: string) => void,
|
||||
defaultValues: { [key: string]: string },
|
||||
disabledFields: { [key: string]: boolean } | string[],
|
||||
isCopy?: boolean,
|
||||
onSubmitCallback?: (values: any, tableName: string) => void,
|
||||
overrideHeading?: string,
|
||||
saveButtonLabel?: string,
|
||||
saveButtonIcon?: string,
|
||||
}
|
||||
|
||||
EntityForm.defaultProps = {
|
||||
@ -79,6 +81,8 @@ EntityForm.defaultProps = {
|
||||
disabledFields: {},
|
||||
isCopy: false,
|
||||
onSubmitCallback: null,
|
||||
saveButtonLabel: "Save",
|
||||
saveButtonIcon: "save",
|
||||
};
|
||||
|
||||
|
||||
@ -173,7 +177,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
*******************************************************************************/
|
||||
function openAddChildRecord(name: string, widgetData: any)
|
||||
{
|
||||
let defaultValues = widgetData.defaultValuesForNewChildRecords;
|
||||
let defaultValues = widgetData.defaultValuesForNewChildRecords || {};
|
||||
|
||||
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
|
||||
if (!disabledFields)
|
||||
@ -181,6 +185,18 @@ function EntityForm(props: Props): JSX.Element
|
||||
disabledFields = widgetData.defaultValuesForNewChildRecords;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy values from specified fields in the parent record down into the child record //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if (widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
for (let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||
defaultValues[childField] = formValues[parentField];
|
||||
}
|
||||
}
|
||||
|
||||
doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields);
|
||||
}
|
||||
|
||||
@ -208,7 +224,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
function deleteChildRecord(name: string, widgetData: any, rowIndex: number)
|
||||
{
|
||||
updateChildRecordList(name, "delete", rowIndex);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -243,16 +259,16 @@ function EntityForm(props: Props): JSX.Element
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function submitEditChildForm(values: any)
|
||||
function submitEditChildForm(values: any, tableName: string)
|
||||
{
|
||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values, tableName);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
async function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
|
||||
async function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any, childTableName?: string)
|
||||
{
|
||||
const metaData = await qController.loadMetaData();
|
||||
const widgetMetaData = metaData.widgets.get(widgetName);
|
||||
@ -263,13 +279,38 @@ function EntityForm(props: Props): JSX.Element
|
||||
newChildListWidgetData[widgetName].queryOutput.records = [];
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// build a map of display values for the new record, specifically, for any possible-values that need translated. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const displayValues: { [fieldName: string]: string } = {};
|
||||
if (childTableName && values)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const childTableMetaData = await qController.loadTableMetaData(childTableName);
|
||||
for (let key in values)
|
||||
{
|
||||
const value = values[key];
|
||||
const field = childTableMetaData.fields.get(key);
|
||||
if (field.possibleValueSourceName)
|
||||
{
|
||||
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], null, objectToMap(values), "form");
|
||||
if (possibleValues && possibleValues.length > 0)
|
||||
{
|
||||
displayValues[key] = possibleValues[0].label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "insert":
|
||||
newChildListWidgetData[widgetName].queryOutput.records.push({values: values});
|
||||
newChildListWidgetData[widgetName].queryOutput.records.push({values: values, displayValues: displayValues});
|
||||
break;
|
||||
case "edit":
|
||||
newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values};
|
||||
newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values, displayValues: displayValues};
|
||||
break;
|
||||
case "delete":
|
||||
newChildListWidgetData[widgetName].queryOutput.records.splice(rowIndex, 1);
|
||||
@ -407,6 +448,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData}
|
||||
recordValues={formValues}
|
||||
label={tableMetaData?.fields.get(widgetData?.filterFieldName ?? "queryFilterJson")?.label}
|
||||
onSaveCallback={setFormFieldValuesFromWidget}
|
||||
/>;
|
||||
}
|
||||
@ -478,6 +520,25 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function objectToMap(object: { [key: string]: any }): Map<string, any>
|
||||
{
|
||||
if (object == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
const rs = new Map<string, any>();
|
||||
for (let key in object)
|
||||
{
|
||||
rs.set(key, object[key]);
|
||||
}
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
//////////////////
|
||||
// initial load //
|
||||
//////////////////
|
||||
@ -595,18 +656,24 @@ function EntityForm(props: Props): JSX.Element
|
||||
if (defaultValue)
|
||||
{
|
||||
initialValues[fieldName] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we need to set the initialDisplayValue for possible value fields with a default value //
|
||||
// so, look them up here now if needed //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (fieldMetaData.possibleValueSourceName)
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// do a second loop, this time looking up display-values for any possible-value fields with a default value //
|
||||
// do it in a second loop, to pass in all the other values (from initialValues), in case there's a PVS filter that needs them. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for (let i = 0; i < fieldArray.length; i++)
|
||||
{
|
||||
const fieldMetaData = fieldArray[i];
|
||||
const fieldName = fieldMetaData.name;
|
||||
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
||||
if (defaultValue && fieldMetaData.possibleValueSourceName)
|
||||
{
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], null, objectToMap(initialValues), "form");
|
||||
if (results && results.length > 0)
|
||||
{
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
|
||||
if (results && results.length > 0)
|
||||
{
|
||||
defaultDisplayValues.set(fieldName, results[0].label);
|
||||
}
|
||||
defaultDisplayValues.set(fieldName, results[0].label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -823,7 +890,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (props.onSubmitCallback)
|
||||
{
|
||||
props.onSubmitCallback(values);
|
||||
props.onSubmitCallback(values, tableName);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1268,12 +1335,14 @@ function EntityForm(props: Props): JSX.Element
|
||||
</Box>
|
||||
)) : null}
|
||||
|
||||
<Box component="div" p={3}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
|
||||
<QSaveButton disabled={isSubmitting} />
|
||||
</Grid>
|
||||
</Box>
|
||||
{formFields &&
|
||||
<Box component="div" p={3} className={props.isModal ? "modalBottomButtonBar" : "stickyBottomButtonBar"}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
<QCancelButton onClickHandler={props.isModal ? props.closeModalHandler : handleCancelClicked} disabled={isSubmitting} />
|
||||
<QSaveButton disabled={isSubmitting} label={props.saveButtonLabel} iconName={props.saveButtonIcon} />
|
||||
</Grid>
|
||||
</Box>
|
||||
}
|
||||
|
||||
</Form>
|
||||
);
|
||||
@ -1292,6 +1361,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
disabledFields={showEditChildForm.disabledFields}
|
||||
onSubmitCallback={props.onSubmitCallback ? props.onSubmitCallback : submitEditChildForm}
|
||||
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
|
||||
saveButtonLabel="OK"
|
||||
saveButtonIcon="check"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -270,7 +270,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
{
|
||||
pageHeader &&
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||
<MDTypography pb="0.5rem" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||
{pageHeader}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
|
@ -20,21 +20,22 @@
|
||||
*/
|
||||
|
||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||
import {Button} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Link from "@mui/material/Link";
|
||||
import List from "@mui/material/List";
|
||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||
import {NavLink, useLocation} from "react-router-dom";
|
||||
import AuthenticationButton from "qqq/components/buttons/AuthenticationButton";
|
||||
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
||||
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
||||
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";
|
||||
|
||||
|
||||
interface Props
|
||||
@ -44,6 +45,7 @@ interface Props
|
||||
logo?: string;
|
||||
appName?: string;
|
||||
branding?: QBrandingMetaData;
|
||||
logout: () => void;
|
||||
routes: {
|
||||
[key: string]:
|
||||
| ReactNode
|
||||
@ -66,7 +68,7 @@ interface Props
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element
|
||||
function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}: Props): JSX.Element
|
||||
{
|
||||
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
||||
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
||||
@ -257,7 +259,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
||||
active={key === collapseName}
|
||||
open={openCollapse === key}
|
||||
noCollapse={noCollapse}
|
||||
onClick={() => (! noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null) }
|
||||
onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
|
||||
>
|
||||
{collapse ? renderCollapse(collapse) : null}
|
||||
</SideNavCollapse>
|
||||
@ -300,6 +302,30 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
||||
}
|
||||
);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function EnvironmentBanner({branding}: { branding: QBrandingMetaData }): JSX.Element | null
|
||||
{
|
||||
// deprecated!
|
||||
if (branding && branding.environmentBannerText)
|
||||
{
|
||||
return <Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||
{branding.environmentBannerText}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO");
|
||||
if (banner)
|
||||
{
|
||||
return <Box className={getBannerClassName(banner)} mt={2} borderRadius={2} sx={getBannerStyles(banner)}>
|
||||
{makeBannerContent(banner)}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidenavRoot
|
||||
{...rest}
|
||||
@ -330,12 +356,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
{
|
||||
branding && branding.environmentBannerText &&
|
||||
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||
{branding.environmentBannerText}
|
||||
</Box>
|
||||
}
|
||||
<EnvironmentBanner branding={branding} />
|
||||
</Box>
|
||||
<Divider
|
||||
light={
|
||||
@ -350,7 +371,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
||||
(darkMode && !transparentSidenav && whiteSidenav)
|
||||
}
|
||||
/>
|
||||
<AuthenticationButton />
|
||||
<Button onClick={logout}>Log Out</Button>
|
||||
</SidenavRoot>
|
||||
);
|
||||
}
|
||||
|
@ -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()),
|
||||
},
|
||||
|
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 {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
|
||||
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";
|
||||
@ -27,25 +28,26 @@ import {Box} from "@mui/material";
|
||||
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {ReactNode, useState} from "react";
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
|
||||
interface FieldAutoCompleteProps
|
||||
{
|
||||
id: string;
|
||||
metaData: QInstance;
|
||||
tableMetaData: QTableMetaData;
|
||||
handleFieldChange: (event: any, newValue: any, reason: string) => void;
|
||||
defaultValue?: { field: QFieldMetaData, table: QTableMetaData, fieldName: string };
|
||||
autoFocus?: boolean;
|
||||
forceOpen?: boolean;
|
||||
hiddenFieldNames?: string[];
|
||||
availableFieldNames?: string[];
|
||||
variant?: "standard" | "filled" | "outlined";
|
||||
label?: string;
|
||||
textFieldSX?: any;
|
||||
autocompleteSlotProps?: any;
|
||||
hasError?: boolean;
|
||||
noOptionsText?: string;
|
||||
id: string,
|
||||
metaData: QInstance,
|
||||
tableMetaData: QTableMetaData,
|
||||
handleFieldChange: (event: any, newValue: any, reason: string) => void,
|
||||
defaultValue?: { field: QFieldMetaData, table: QTableMetaData, fieldName: string },
|
||||
autoFocus?: boolean,
|
||||
forceOpen?: boolean,
|
||||
hiddenFieldNames?: string[],
|
||||
availableFieldNames?: string[],
|
||||
variant?: "standard" | "filled" | "outlined",
|
||||
label?: string,
|
||||
textFieldSX?: any,
|
||||
autocompleteSlotProps?: any,
|
||||
hasError?: boolean,
|
||||
noOptionsText?: string,
|
||||
omitExposedJoins?: string[]
|
||||
}
|
||||
|
||||
FieldAutoComplete.defaultProps =
|
||||
@ -88,7 +90,7 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a
|
||||
/*******************************************************************************
|
||||
** Component for rendering a list of field names from a table as an auto-complete.
|
||||
*******************************************************************************/
|
||||
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText}: FieldAutoCompleteProps): JSX.Element
|
||||
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText, omitExposedJoins}: FieldAutoCompleteProps): JSX.Element
|
||||
{
|
||||
const [selectedFieldName, setSelectedFieldName] = useState(defaultValue ? defaultValue.fieldName : null);
|
||||
|
||||
@ -96,11 +98,25 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
|
||||
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames, availableFieldNames, selectedFieldName);
|
||||
let fieldsGroupBy = null;
|
||||
|
||||
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
|
||||
const availableExposedJoins = useMemo(() =>
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
const rs: QExposedJoin[] = []
|
||||
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
|
||||
{
|
||||
const exposedJoin = tableMetaData.exposedJoins[i];
|
||||
if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
rs.push(exposedJoin);
|
||||
}
|
||||
return (rs);
|
||||
}, [tableMetaData, omitExposedJoins]);
|
||||
|
||||
if (availableExposedJoins && availableExposedJoins.length > 0)
|
||||
{
|
||||
for (let i = 0; i < availableExposedJoins.length; i++)
|
||||
{
|
||||
const exposedJoin = availableExposedJoins[i];
|
||||
if (metaData.tables.has(exposedJoin.joinTable.name))
|
||||
{
|
||||
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
|
||||
@ -185,7 +201,7 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
|
||||
{originalEndAdornment}
|
||||
</Box>;
|
||||
|
||||
return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />)
|
||||
return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />);
|
||||
}}
|
||||
// @ts-ignore
|
||||
defaultValue={defaultValue}
|
||||
|
@ -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 //
|
||||
|
@ -21,12 +21,11 @@
|
||||
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {Checkbox, FormControlLabel, Radio} from "@mui/material";
|
||||
import {Checkbox, FormControlLabel, Radio, Tooltip} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import 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";
|
||||
@ -34,6 +33,7 @@ import colors from "qqq/assets/theme/base/colors";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import {BulkLoadField, FileDescription} from "qqq/models/processes/BulkLoadModels";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
interface BulkLoadMappingFieldProps
|
||||
@ -45,6 +45,29 @@ interface BulkLoadMappingFieldProps
|
||||
forceParentUpdate?: () => void,
|
||||
}
|
||||
|
||||
const xIconButtonSX =
|
||||
{
|
||||
border: `1px solid ${colors.grayLines.main} !important`,
|
||||
borderRadius: "0.5rem",
|
||||
textTransform: "none",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "400",
|
||||
width: "30px",
|
||||
minWidth: "30px",
|
||||
height: "2rem",
|
||||
minHeight: "2rem",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
marginRight: "0.5rem",
|
||||
marginTop: "0.5rem",
|
||||
color: colors.error.main,
|
||||
"&:hover": {color: colors.error.main},
|
||||
"&:focus": {color: colors.error.main},
|
||||
"&:focus:not(:hover)": {color: colors.error.main},
|
||||
};
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
/***************************************************************************
|
||||
** row for a single field on the bulk load mapping screen.
|
||||
***************************************************************************/
|
||||
@ -54,6 +77,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
|
||||
const [valueType, setValueType] = useState(bulkLoadField.valueType);
|
||||
const [selectedColumn, setSelectedColumn] = useState({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
||||
const [selectedColumnInputValue, setSelectedColumnInputValue] = useState(columnNames[bulkLoadField.columnIndex]);
|
||||
|
||||
const [doingInitialLoadOfPossibleValue, setDoingInitialLoadOfPossibleValue] = useState(false);
|
||||
const [everDidInitialLoadOfPossibleValue, setEverDidInitialLoadOfPossibleValue] = useState(false);
|
||||
const [possibleValueInitialDisplayValue, setPossibleValueInitialDisplayValue] = useState(null as string);
|
||||
|
||||
const fieldMetaData = new QFieldMetaData(bulkLoadField.field);
|
||||
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||
@ -61,18 +89,68 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
dynamicFieldInObject[fieldMetaData["name"]] = dynamicField;
|
||||
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [fieldMetaData], bulkLoadField.tableStructure.tableName, null, null);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// deal with dynamically loading the initial default value for a possible value... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
|
||||
if (dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
|
||||
{
|
||||
actuallyDoingInitialLoadOfPossibleValue = true;
|
||||
setDoingInitialLoadOfPossibleValue(true);
|
||||
setEverDidInitialLoadOfPossibleValue(true);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, null, "filter");
|
||||
if (possibleValues && possibleValues.length > 0)
|
||||
{
|
||||
setPossibleValueInitialDisplayValue(possibleValues[0].label);
|
||||
}
|
||||
else
|
||||
{
|
||||
setPossibleValueInitialDisplayValue(null);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error loading possible value: ${e}`);
|
||||
}
|
||||
|
||||
actuallyDoingInitialLoadOfPossibleValue = false;
|
||||
setDoingInitialLoadOfPossibleValue(false);
|
||||
})();
|
||||
}
|
||||
|
||||
if (dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
|
||||
{
|
||||
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// build array of options for the columns drop down //
|
||||
// don't allow duplicates //
|
||||
//////////////////////////////////////////////////////
|
||||
const columnOptions: { value: number, label: string }[] = [];
|
||||
const usedLabels: { [label: string]: boolean } = {};
|
||||
for (let i = 0; i < columnNames.length; i++)
|
||||
{
|
||||
columnOptions.push({label: columnNames[i], value: i});
|
||||
const label = columnNames[i];
|
||||
if (!usedLabels[label])
|
||||
{
|
||||
columnOptions.push({label: label, value: i});
|
||||
usedLabels[label] = true;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// try to pick up changes in the hasHeaderRow toggle from way above //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
if(bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
|
||||
if (bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
|
||||
{
|
||||
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex})
|
||||
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
||||
setSelectedColumnInputValue(columnNames[bulkLoadField.columnIndex]);
|
||||
}
|
||||
|
||||
const mainFontSize = "0.875rem";
|
||||
@ -98,6 +176,8 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
function columnChanged(event: any, newValue: any, reason: string)
|
||||
{
|
||||
setSelectedColumn(newValue);
|
||||
setSelectedColumnInputValue(newValue == null ? "" : newValue.label);
|
||||
|
||||
bulkLoadField.columnIndex = newValue == null ? null : newValue.value;
|
||||
|
||||
if (fileDescription.hasHeaderRow)
|
||||
@ -106,6 +186,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
}
|
||||
|
||||
bulkLoadField.error = null;
|
||||
bulkLoadField.warning = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
@ -118,6 +199,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
setFieldValue(`${bulkLoadField.field.name}.defaultValue`, newValue);
|
||||
bulkLoadField.defaultValue = newValue;
|
||||
bulkLoadField.error = null;
|
||||
bulkLoadField.warning = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
@ -131,6 +213,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
bulkLoadField.valueType = newValueType;
|
||||
setValueType(newValueType);
|
||||
bulkLoadField.error = null;
|
||||
bulkLoadField.warning = null;
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
@ -144,7 +227,15 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
forceParentUpdate && forceParentUpdate();
|
||||
}
|
||||
|
||||
return (<Box py="0.5rem" sx={{borderBottom: "1px solid lightgray", width: "100%", overflow: "auto"}}>
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function changeSelectedColumnInputValue(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>)
|
||||
{
|
||||
setSelectedColumnInputValue(e.target.value);
|
||||
}
|
||||
|
||||
return (<Box py="0.5rem" sx={{borderBottom: "1px solid lightgray", width: "100%", overflow: "auto"}} id={`blfmf-${bulkLoadField.field.name}`}>
|
||||
<Box display="grid" gridTemplateColumns="200px 400px auto" fontSize="1rem" gap="0.5rem" sx={
|
||||
{
|
||||
"& .MuiFormControlLabel-label": {ml: "0 !important", fontWeight: "normal !important", fontSize: mainFontSize}
|
||||
@ -152,7 +243,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
|
||||
<Box display="flex" alignItems="flex-start">
|
||||
{
|
||||
(!isRequired) && <IconButton onClick={() => removeFieldCallback()} sx={{pt: "0.75rem"}}><Icon fontSize="small">remove_circle</Icon></IconButton>
|
||||
(!isRequired) && <Tooltip placement="bottom" title="Remove this field from your mapping.">
|
||||
<Button sx={xIconButtonSX} onClick={() => removeFieldCallback()}><Icon>clear</Icon></Button>
|
||||
</Tooltip>
|
||||
}
|
||||
<Box pt="0.625rem">
|
||||
{bulkLoadField.getQualifiedLabel()}
|
||||
@ -167,13 +260,13 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
valueType == "column" && <Box width="100%">
|
||||
<Autocomplete
|
||||
id={bulkLoadField.field.name}
|
||||
renderInput={(params) => (<TextField {...params} label={""} value={selectedColumn?.label} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||
renderInput={(params) => (<TextField {...params} label={""} value={selectedColumnInputValue} onChange={e => changeSelectedColumnInputValue(e)} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||
fullWidth
|
||||
options={columnOptions}
|
||||
multiple={false}
|
||||
defaultValue={selectedColumn}
|
||||
value={selectedColumn}
|
||||
inputValue={selectedColumn?.label}
|
||||
inputValue={selectedColumnInputValue}
|
||||
onChange={columnChanged}
|
||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.value == value.value}
|
||||
@ -186,7 +279,10 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||
<FormControlLabel value="defaultValue" control={<Radio size="small" onChange={(event, checked) => valueTypeChanged(!checked)} />} label={"Default value"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||
{
|
||||
valueType == "defaultValue" && <Box width="100%">
|
||||
valueType == "defaultValue" && actuallyDoingInitialLoadOfPossibleValue && <Box width="100%">Loading...</Box>
|
||||
}
|
||||
{
|
||||
valueType == "defaultValue" && !actuallyDoingInitialLoadOfPossibleValue && <Box width="100%">
|
||||
<QDynamicFormField
|
||||
name={`${bulkLoadField.field.name}.defaultValue`}
|
||||
displayFormat={""}
|
||||
@ -200,9 +296,15 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{
|
||||
bulkLoadField.warning &&
|
||||
<Box fontSize={smallerFontSize} color={colors.warning.main} ml="145px" className="bulkLoadFieldError">
|
||||
{bulkLoadField.warning}
|
||||
</Box>
|
||||
}
|
||||
{
|
||||
bulkLoadField.error &&
|
||||
<Box fontSize={smallerFontSize} color={colors.error.main} ml="145px">
|
||||
<Box fontSize={smallerFontSize} color={colors.error.main} ml="145px" className="bulkLoadFieldError">
|
||||
{bulkLoadField.error}
|
||||
</Box>
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
{
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
const [forceRerender, setForceRerender] = useState(0);
|
||||
const [forceHierarchyAutoCompleteRerender, setForceHierarchyAutoCompleteRerender] = useState(0);
|
||||
|
||||
////////////////////////////////////////////
|
||||
// build list of fields that can be added //
|
||||
@ -98,8 +98,9 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
|
||||
setAddFieldsDisableStates(newDisableStates);
|
||||
setTooltips(newTooltips);
|
||||
setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1);
|
||||
|
||||
}, [bulkLoadMapping]);
|
||||
}, [bulkLoadMapping, bulkLoadMapping.layout]);
|
||||
|
||||
|
||||
///////////////////////////////////////////////
|
||||
@ -140,9 +141,6 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
***************************************************************************/
|
||||
function removeField(bulkLoadField: BulkLoadField)
|
||||
{
|
||||
// addFieldsToggleStates[bulkLoadField.getQualifiedName()] = false;
|
||||
// setAddFieldsToggleStates(Object.assign({}, addFieldsToggleStates));
|
||||
|
||||
addFieldsDisableStates[bulkLoadField.getQualifiedName()] = false;
|
||||
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||
|
||||
@ -160,7 +158,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
bulkLoadMapping.removeField(bulkLoadField);
|
||||
forceUpdate();
|
||||
forceParentUpdate();
|
||||
setForceRerender(forceRerender + 1);
|
||||
setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
@ -297,7 +295,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
||||
isModeSelectOne
|
||||
keepOpenAfterSelectOne
|
||||
handleSelectedOption={handleAddField}
|
||||
forceRerender={forceRerender}
|
||||
forceRerender={forceHierarchyAutoCompleteRerender}
|
||||
disabledStates={addFieldsDisableStates}
|
||||
tooltips={tooltips}
|
||||
/>
|
||||
|
@ -20,45 +20,56 @@
|
||||
*/
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Badge, Icon} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {useFormikContext} from "formik";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
||||
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
|
||||
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
||||
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
||||
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
|
||||
import React, {forwardRef, useImperativeHandle, useReducer, useState} from "react";
|
||||
import ProcessViewForm from "./ProcessViewForm";
|
||||
|
||||
|
||||
interface BulkLoadMappingFormProps
|
||||
{
|
||||
processValues: any;
|
||||
tableMetaData: QTableMetaData;
|
||||
metaData: QInstance;
|
||||
setActiveStepLabel: (label: string) => void;
|
||||
processValues: any,
|
||||
tableMetaData: QTableMetaData,
|
||||
metaData: QInstance,
|
||||
setActiveStepLabel: (label: string) => void,
|
||||
frontendStep: QFrontendStepMetaData,
|
||||
processMetaData: QProcessMetaData,
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** process component - screen where user does a bulk-load file mapping.
|
||||
***************************************************************************/
|
||||
const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaData, setActiveStepLabel}: BulkLoadMappingFormProps, ref) =>
|
||||
const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaData, setActiveStepLabel, frontendStep, processMetaData}: BulkLoadMappingFormProps, ref) =>
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
|
||||
const [currentSavedBulkLoadProfile, setCurrentSavedBulkLoadProfile] = useState(null as QRecord);
|
||||
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
||||
const [currentSavedBulkLoadProfile, setCurrentSavedBulkLoadProfile] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue));
|
||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(currentSavedBulkLoadProfile));
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState({} as { [fieldName: string]: string });
|
||||
const [noMappedFieldsError, setNoMappedFieldsError] = useState(null as string);
|
||||
|
||||
const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile);
|
||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||
@ -119,18 +130,31 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
}
|
||||
setFieldErrors(fieldErrors);
|
||||
|
||||
if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
|
||||
{
|
||||
setNoMappedFieldsError("You must have at least 1 field.");
|
||||
haveLocalErrors = true;
|
||||
setTimeout(() => setNoMappedFieldsError(null), 2500);
|
||||
}
|
||||
else
|
||||
{
|
||||
setNoMappedFieldsError(null);
|
||||
}
|
||||
|
||||
if(haveProfileErrors)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
return {maySubmit: !haveProfileErrors && !haveLocalErrors, values};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
console.log("@dk has header row changed!");
|
||||
}, [bulkLoadMapping.hasHeaderRow]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@ -214,6 +238,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
tableStructure={tableStructure}
|
||||
fileName={processValues.fileBaseName}
|
||||
fieldErrors={fieldErrors}
|
||||
frontendStep={frontendStep}
|
||||
processMetaData={processMetaData}
|
||||
forceParentUpdate={() => forceUpdate()}
|
||||
/>
|
||||
|
||||
@ -221,8 +247,15 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
<BulkLoadFileMappingFields
|
||||
bulkLoadMapping={bulkLoadMapping}
|
||||
fileDescription={fileDescription}
|
||||
forceParentUpdate={() => forceUpdate()}
|
||||
forceParentUpdate={() =>
|
||||
{
|
||||
setRerenderHeader(rerenderHeader + 1);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
{
|
||||
noMappedFieldsError && <Box color={colors.error.main} textAlign="right">{noMappedFieldsError}</Box>
|
||||
}
|
||||
</Box>
|
||||
|
||||
</Box>);
|
||||
@ -232,8 +265,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
||||
export default BulkLoadFileMappingForm;
|
||||
|
||||
|
||||
|
||||
|
||||
interface BulkLoadMappingHeaderProps
|
||||
{
|
||||
fileDescription: FileDescription,
|
||||
@ -241,13 +272,15 @@ interface BulkLoadMappingHeaderProps
|
||||
bulkLoadMapping?: BulkLoadMapping,
|
||||
fieldErrors: { [fieldName: string]: string },
|
||||
tableStructure: BulkLoadTableStructure,
|
||||
forceParentUpdate?: () => void
|
||||
forceParentUpdate?: () => void,
|
||||
frontendStep: QFrontendStepMetaData,
|
||||
processMetaData: QProcessMetaData,
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** private subcomponent - the header section of the bulk load file mapping screen.
|
||||
***************************************************************************/
|
||||
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate}: BulkLoadMappingHeaderProps): JSX.Element
|
||||
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element
|
||||
{
|
||||
const viewFields = [
|
||||
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
||||
@ -261,8 +294,6 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
|
||||
const hasHeaderRowFormField = {name: "hasHeaderRow", label: "Does the file have a header row?", type: "checkbox", isRequired: true, isEditable: true};
|
||||
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
|
||||
const layoutOptions = [
|
||||
{label: "Flat", id: "FLAT"},
|
||||
{label: "Tall", id: "TALL"},
|
||||
@ -276,27 +307,55 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
|
||||
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null;
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function hasHeaderRowChanged(newValue: any)
|
||||
{
|
||||
bulkLoadMapping.hasHeaderRow = newValue;
|
||||
fileDescription.hasHeaderRow = newValue;
|
||||
|
||||
bulkLoadMapping.handleChangeToHasHeaderRow(newValue, fileDescription);
|
||||
|
||||
fieldErrors.hasHeaderRow = null;
|
||||
forceParentUpdate();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function layoutChanged(event: any, newValue: any)
|
||||
{
|
||||
bulkLoadMapping.layout = newValue ? newValue.id : null;
|
||||
bulkLoadMapping.switchLayout(newValue ? newValue.id : null);
|
||||
fieldErrors.layout = null;
|
||||
forceParentUpdate();
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getFormattedHelpContent(fieldName: string): JSX.Element
|
||||
{
|
||||
const field = frontendStep?.formFields?.find(f => f.name == fieldName);
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
|
||||
let formattedHelpContent = <HelpContent helpContents={field?.helpContents} roles={helpRoles} helpContentKey={`process:${processMetaData?.name};field:${fieldName}`} />;
|
||||
if (formattedHelpContent)
|
||||
{
|
||||
const mt = field && field.type == QFieldType.BOOLEAN ? "-0.5rem" : "0.5rem";
|
||||
|
||||
return <Box color="#757575" fontSize="0.875rem" mt={mt}>{formattedHelpContent}</Box>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<h5>File Details</h5>
|
||||
<Box ml="1rem">
|
||||
<ProcessViewForm fields={viewFields} values={viewValues} columns={2} />
|
||||
<BulkLoadMappingFilePreview fileDescription={fileDescription} />
|
||||
<BulkLoadMappingFilePreview fileDescription={fileDescription} bulkLoadMapping={bulkLoadMapping} />
|
||||
<Grid container pt="1rem">
|
||||
<Grid item xs={12} md={6}>
|
||||
<DynamicFormFieldLabel name={hasHeaderRowFormField.name} label={`${hasHeaderRowFormField.label} *`} />
|
||||
@ -307,6 +366,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
{<div className="fieldErrorMessage">{fieldErrors.hasHeaderRow}</div>}
|
||||
</MDTypography>
|
||||
}
|
||||
{getFormattedHelpContent("hasHeaderRow")}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
||||
@ -320,6 +380,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
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"}}}
|
||||
/>
|
||||
{
|
||||
@ -328,6 +389,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
||||
</MDTypography>
|
||||
}
|
||||
{getFormattedHelpContent("layout")}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
@ -336,16 +398,16 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface BulkLoadMappingFilePreviewProps
|
||||
{
|
||||
fileDescription: FileDescription;
|
||||
fileDescription: FileDescription,
|
||||
bulkLoadMapping?: BulkLoadMapping
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
** private subcomponent - the file-preview section of the bulk load file mapping screen.
|
||||
***************************************************************************/
|
||||
function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePreviewProps): JSX.Element
|
||||
function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoadMappingFilePreviewProps): JSX.Element
|
||||
{
|
||||
const rows: number[] = [];
|
||||
for (let i = 0; i < fileDescription.bodyValuesPreview[0].length; i++)
|
||||
@ -353,25 +415,145 @@ function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePrevie
|
||||
rows.push(i);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getValue(i: number, j: number)
|
||||
{
|
||||
const value = fileDescription.bodyValuesPreview[j][i];
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this was useful at one point in time when we had an object coming back for xlsx files with many different data types //
|
||||
// we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// @ts-ignore
|
||||
if (value && value.string)
|
||||
{
|
||||
// @ts-ignore
|
||||
return (value.string);
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getHeaderColor(count: number): string
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
return "blue";
|
||||
}
|
||||
|
||||
return "black";
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getCursor(count: number): string
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
return "pointer";
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getColumnTooltip(fields: BulkLoadField[])
|
||||
{
|
||||
return (<Box>
|
||||
This column is mapped to the field{fields.length == 1 ? "" : "s"}:
|
||||
<ul style={{marginLeft: "1rem"}}>
|
||||
{fields.map((field, i) => <li key={i}>{field.getQualifiedLabel()}</li>)}
|
||||
</ul>
|
||||
</Box>);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{"& table, & td": {border: "1px solid black", borderCollapse: "collapse", padding: "0 0.25rem", fontSize: "0.875rem", whiteSpace: "nowrap"}}}>
|
||||
<Box sx={{width: "100%", overflow: "auto"}}>
|
||||
<table cellSpacing="0" width="100%">
|
||||
<thead>
|
||||
<tr style={{backgroundColor: "#d3d3d3"}}>
|
||||
<tr style={{backgroundColor: "#d3d3d3", height: "1.75rem"}}>
|
||||
<td></td>
|
||||
{fileDescription.headerLetters.map((letter) => <td key={letter} style={{textAlign: "center"}}>{letter}</td>)}
|
||||
{fileDescription.headerLetters.map((letter, index) =>
|
||||
{
|
||||
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
||||
const count = fields.length;
|
||||
|
||||
let dupeWarning = <></>
|
||||
if(fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index])
|
||||
{
|
||||
dupeWarning = <Tooltip title="This column header is a duplicate. Only the first occurrance of it will be used." placement="top" enterDelay={500}>
|
||||
<Icon color="warning" sx={{p: "0.125rem", mr: "0.25rem"}}>warning</Icon>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
|
||||
<>
|
||||
{
|
||||
count > 0 &&
|
||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
||||
<Box>
|
||||
{dupeWarning}
|
||||
{letter}
|
||||
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
count == 0 && <Box>{dupeWarning}{letter}</Box>
|
||||
}
|
||||
</>
|
||||
</td>);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>1</td>
|
||||
{fileDescription.headerValues.map((value) => <td key={value} style={{backgroundColor: fileDescription.hasHeaderRow ? "#ebebeb" : ""}}>{value}</td>)}
|
||||
|
||||
{fileDescription.headerValues.map((value, index) =>
|
||||
{
|
||||
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
||||
const count = fields.length;
|
||||
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
|
||||
|
||||
if(fileDescription.hasHeaderRow)
|
||||
{
|
||||
tdStyle.backgroundColor = "#ebebeb";
|
||||
|
||||
if(count > 0)
|
||||
{
|
||||
return <td key={value} style={tdStyle}>
|
||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
return <td key={value} style={tdStyle}>{value}</td>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return <td key={value} style={tdStyle}>{value}</td>
|
||||
}
|
||||
}
|
||||
)}
|
||||
</tr>
|
||||
{rows.map((i) => (
|
||||
<tr key={i}>
|
||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>{i + 2}</td>
|
||||
{fileDescription.headerLetters.map((letter, j) => <td key={j}>{fileDescription.bodyValuesPreview[j][i]}</td>)}
|
||||
{fileDescription.headerLetters.map((letter, j) => <td key={j}>{getValue(i, j)}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -266,76 +266,6 @@ function ValidationReview({
|
||||
</List>
|
||||
);
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function previewRecordUsingTableLayout(record: QRecord)
|
||||
{
|
||||
if (!previewTableMetaData)
|
||||
{
|
||||
return (<Box>Loading...</Box>);
|
||||
}
|
||||
|
||||
const renderedSections: JSX.Element[] = [];
|
||||
const tableSections = TableUtils.getSectionsForRecordSidebar(previewTableMetaData);
|
||||
const previewRecord = previewRecords[previewRecordIndex];
|
||||
|
||||
for (let i = 0; i < tableSections.length; i++)
|
||||
{
|
||||
const section = tableSections[i];
|
||||
if (section.isHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.fieldNames)
|
||||
{
|
||||
renderedSections.push(<Box mb="1rem">
|
||||
<Box><h4>{section.label}</h4></Box>
|
||||
<Box ml="1rem">
|
||||
{renderSectionOfFields(section.name, section.fieldNames, previewTableMetaData, false, previewRecord, undefined, {label: {fontWeight: "500"}})}
|
||||
</Box>
|
||||
</Box>);
|
||||
}
|
||||
else if (section.widgetName)
|
||||
{
|
||||
const widget = qInstance.widgets.get(section.widgetName);
|
||||
if (widget)
|
||||
{
|
||||
let data: ChildRecordListData = null;
|
||||
if (associationPreviewsByWidgetName[section.widgetName])
|
||||
{
|
||||
const associationPreview = associationPreviewsByWidgetName[section.widgetName];
|
||||
const associationRecords = previewRecord.associatedRecords.get(associationPreview.associationName) ?? [];
|
||||
data = {
|
||||
canAddChildRecord: false,
|
||||
childTableMetaData: childTableMetaData[associationPreview.tableName],
|
||||
defaultValuesForNewChildRecords: {},
|
||||
disabledFieldsForNewChildRecords: {},
|
||||
queryOutput: {records: associationRecords},
|
||||
totalRows: associationRecords.length,
|
||||
tablePath: "",
|
||||
title: "",
|
||||
viewAllLink: "",
|
||||
};
|
||||
|
||||
renderedSections.push(<Box mb="1rem">
|
||||
{
|
||||
data && <Box>
|
||||
<Box mb="0.5rem"><h4>{section.label}</h4></Box>
|
||||
<Box pl="1rem">
|
||||
<RecordGridWidget data={data} widgetMetaData={widget} disableRowClick gridOnly={true} gridDensity={"compact"} />
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</Box>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderedSections;
|
||||
}
|
||||
|
||||
const recordPreviewWidget = step.recordListFields && (
|
||||
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="10px" p={2} mt={2}>
|
||||
@ -370,11 +300,11 @@ function ValidationReview({
|
||||
{
|
||||
processValues.validationSummary ? (
|
||||
<>
|
||||
It appears as though this process does not contain any valid records.
|
||||
It appears as though this process does not contain any valid records.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
If you choose to Perform Validation, and there are any valid records, then you will see a preview here.
|
||||
If you choose to Perform Validation, and there are any valid records, then you will see a preview here.
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -405,7 +335,15 @@ function ValidationReview({
|
||||
))
|
||||
}
|
||||
{
|
||||
previewRecords && processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] && previewRecordUsingTableLayout(previewRecords[previewRecordIndex])
|
||||
previewRecords && processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] &&
|
||||
<PreviewRecordUsingTableLayout
|
||||
index={previewRecordIndex}
|
||||
record={previewRecords[previewRecordIndex]}
|
||||
tableMetaData={previewTableMetaData}
|
||||
qInstance={qInstance}
|
||||
associationPreviewsByWidgetName={associationPreviewsByWidgetName}
|
||||
childTableMetaData={childTableMetaData}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
@ -441,4 +379,84 @@ function ValidationReview({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface PreviewRecordUsingTableLayoutProps
|
||||
{
|
||||
index: number
|
||||
record: QRecord,
|
||||
tableMetaData: QTableMetaData,
|
||||
qInstance: QInstance,
|
||||
associationPreviewsByWidgetName: { [widgetName: string]: AssociationPreview },
|
||||
childTableMetaData: { [name: string]: QTableMetaData },
|
||||
}
|
||||
|
||||
function PreviewRecordUsingTableLayout({record, tableMetaData, qInstance, associationPreviewsByWidgetName, childTableMetaData, index}: PreviewRecordUsingTableLayoutProps): JSX.Element
|
||||
{
|
||||
if (!tableMetaData)
|
||||
{
|
||||
return (<i>Loading...</i>);
|
||||
}
|
||||
|
||||
const renderedSections: JSX.Element[] = [];
|
||||
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData);
|
||||
|
||||
for (let i = 0; i < tableSections.length; i++)
|
||||
{
|
||||
const section = tableSections[i];
|
||||
if (section.isHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.fieldNames)
|
||||
{
|
||||
renderedSections.push(<Box mb="1rem">
|
||||
<Box><h4>{section.label}</h4></Box>
|
||||
<Box ml="1rem">
|
||||
{renderSectionOfFields(section.name, section.fieldNames, tableMetaData, false, record, undefined, {label: {fontWeight: "500"}})}
|
||||
</Box>
|
||||
</Box>);
|
||||
}
|
||||
else if (section.widgetName)
|
||||
{
|
||||
const widget = qInstance.widgets.get(section.widgetName);
|
||||
if (widget)
|
||||
{
|
||||
let data: ChildRecordListData = null;
|
||||
if (associationPreviewsByWidgetName[section.widgetName])
|
||||
{
|
||||
const associationPreview = associationPreviewsByWidgetName[section.widgetName];
|
||||
const associationRecords = record.associatedRecords?.get(associationPreview.associationName) ?? [];
|
||||
data = {
|
||||
canAddChildRecord: false,
|
||||
childTableMetaData: childTableMetaData[associationPreview.tableName],
|
||||
defaultValuesForNewChildRecords: {},
|
||||
disabledFieldsForNewChildRecords: {},
|
||||
queryOutput: {records: associationRecords},
|
||||
totalRows: associationRecords.length,
|
||||
tablePath: "",
|
||||
title: "",
|
||||
viewAllLink: "",
|
||||
};
|
||||
|
||||
renderedSections.push(<Box mb="1rem">
|
||||
{
|
||||
data && <Box>
|
||||
<Box mb="0.5rem"><h4>{section.label}</h4></Box>
|
||||
<Box pl="1rem">
|
||||
<RecordGridWidget key={index} data={data} widgetMetaData={widget} disableRowClick gridOnly={true} gridDensity={"compact"} />
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</Box>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>{renderedSections}</>;
|
||||
}
|
||||
|
||||
|
||||
export default ValidationReview;
|
||||
|
@ -83,6 +83,8 @@ interface BasicAndAdvancedQueryControlsProps
|
||||
|
||||
mode: string;
|
||||
setMode: (mode: string) => void;
|
||||
|
||||
omitExposedJoins?: string[];
|
||||
}
|
||||
|
||||
let debounceTimeout: string | number | NodeJS.Timeout;
|
||||
@ -627,6 +629,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
handleSelectedField={handleSetSort}
|
||||
fieldEndAdornment={<Box whiteSpace="nowrap"><Icon>arrow_upward</Icon><Icon>arrow_downward</Icon></Box>}
|
||||
handleAdornmentClick={handleSetSortArrowClick}
|
||||
omitExposedJoins={props.omitExposedJoins}
|
||||
/>);
|
||||
|
||||
const filterBuilderMouseEvents =
|
||||
@ -721,6 +724,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
buttonChildren={"Add Filter"}
|
||||
isModeSelectOne={true}
|
||||
handleSelectedField={handleFieldListMenuSelection}
|
||||
omitExposedJoins={props.omitExposedJoins}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
|
@ -43,6 +43,7 @@ declare module "@mui/x-data-grid"
|
||||
metaData: QInstance;
|
||||
queryFilter: QQueryFilter;
|
||||
updateFilter: (newFilter: QQueryFilter) => void;
|
||||
omitExposedJoins?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,6 +182,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
|
||||
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
|
||||
allowVariables={props.allowVariables}
|
||||
queryScreenUsage={props.queryScreenUsage}
|
||||
omitExposedJoins={props.omitExposedJoins}
|
||||
/>
|
||||
{/*JSON.stringify(criteria)*/}
|
||||
</Box>
|
||||
|
@ -26,9 +26,9 @@ import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import {GridRowsProp} from "@mui/x-data-grid-pro";
|
||||
import React from "react";
|
||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
import React from "react";
|
||||
|
||||
interface CustomPaginationProps
|
||||
{
|
||||
@ -56,7 +56,7 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
|
||||
The number of rows shown on this screen may be greater than the number of {tableMetaData?.label} records
|
||||
that match your query, because you have included fields from other tables which may have
|
||||
more than one record associated with each {tableMetaData?.label}.
|
||||
</>
|
||||
</>;
|
||||
let distinctPart = isJoinMany ? (<Box display="inline" component="span" textAlign="right">
|
||||
({ValueUtils.safeToLocaleString(distinctRecords)} distinct<CustomWidthTooltip title={tooltipHTML}>
|
||||
<IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
|
||||
@ -66,13 +66,23 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
|
||||
|
||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
||||
{
|
||||
if (loading)
|
||||
{
|
||||
return "Counting...";
|
||||
}
|
||||
|
||||
if (!rows || rows.length == 0)
|
||||
{
|
||||
return "No rows";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, //
|
||||
// we'll do this... not quite good enough, but better than the original //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (rows.length > 0 && rows.length < to - from)
|
||||
{
|
||||
to = from + rows.length;
|
||||
to = from + (rows.length - 1);
|
||||
}
|
||||
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
|
||||
}
|
||||
@ -102,14 +112,55 @@ export default function CustomPaginationComponent({tableMetaData, rows, totalRec
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// the `count` param that we pass to <TablePagination> below is very //
|
||||
// important - it drives which of the < and > (prev & next) buttons are //
|
||||
// enabled - and, it's a little tricky for tables where we don't do a count. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
let countForTablePagination: number;
|
||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// handle tables where count is disabled. //
|
||||
////////////////////////////////////////////
|
||||
if(!rows || rows.length == 0)
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// if we have no rows, assume a count of 0 //
|
||||
/////////////////////////////////////////////
|
||||
countForTablePagination = 0;
|
||||
}
|
||||
if(rows.length < rowsPerPage)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the # of rows we have is less than the rowsPerPage, assume we're at the end of the query //
|
||||
// so, setting count to pageNo*rowsPer + rows.length - leaves prev. enabled, but disables next. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
countForTablePagination = (pageNumber * rowsPerPage) + rows.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, we don't know how many more pages there could be - so, just assume it's at least 1 more //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
countForTablePagination = ((pageNumber + 1) * rowsPerPage) + 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// cases where count is enabled - they work much more like we'd expect: //
|
||||
// if we don't know totalRecords (probably same as loading?) - use a -1, //
|
||||
// which lets us see < and > both active; else, use totalRecords when known. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
countForTablePagination = totalRecords === null || totalRecords === undefined ? -1 : totalRecords;
|
||||
}
|
||||
|
||||
return (
|
||||
<TablePagination
|
||||
component="div"
|
||||
sx={{minWidth: "450px"}}
|
||||
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
|
||||
// so pass a sentinel value of -1...
|
||||
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
|
||||
sx={{minWidth: "450px", "& .MuiTablePagination-displayedRows": {minWidth: "110px"}}}
|
||||
count={countForTablePagination}
|
||||
page={pageNumber}
|
||||
rowsPerPageOptions={[10, 25, 50, 100, 250]}
|
||||
rowsPerPage={rowsPerPage}
|
||||
|
@ -19,6 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import Box from "@mui/material/Box";
|
||||
@ -31,28 +32,26 @@ import ListItem, {ListItemProps} from "@mui/material/ListItem/ListItem";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {useState} from "react";
|
||||
import React, {useMemo, useState} from "react";
|
||||
|
||||
interface FieldListMenuProps
|
||||
{
|
||||
idPrefix: string;
|
||||
heading?: string;
|
||||
placeholder?: string;
|
||||
tableMetaData: QTableMetaData;
|
||||
showTableHeaderEvenIfNoExposedJoins: boolean;
|
||||
fieldNamesToHide?: string[];
|
||||
buttonProps: any;
|
||||
buttonChildren: JSX.Element | string;
|
||||
|
||||
isModeSelectOne?: boolean;
|
||||
handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void;
|
||||
|
||||
isModeToggle?: boolean;
|
||||
toggleStates?: {[fieldName: string]: boolean};
|
||||
handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void;
|
||||
|
||||
fieldEndAdornment?: JSX.Element
|
||||
handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void;
|
||||
idPrefix: string,
|
||||
heading?: string,
|
||||
placeholder?: string,
|
||||
tableMetaData: QTableMetaData,
|
||||
showTableHeaderEvenIfNoExposedJoins: boolean,
|
||||
fieldNamesToHide?: string[],
|
||||
buttonProps: any,
|
||||
buttonChildren: JSX.Element | string,
|
||||
isModeSelectOne?: boolean,
|
||||
handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void,
|
||||
isModeToggle?: boolean,
|
||||
toggleStates?: { [fieldName: string]: boolean },
|
||||
handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void,
|
||||
fieldEndAdornment?: JSX.Element,
|
||||
handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void,
|
||||
omitExposedJoins?: string[]
|
||||
}
|
||||
|
||||
FieldListMenu.defaultProps = {
|
||||
@ -71,38 +70,52 @@ interface TableWithFields
|
||||
** Component to render a list of fields from a table (and its join tables)
|
||||
** which can be interacted with...
|
||||
*******************************************************************************/
|
||||
export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick}: FieldListMenuProps): JSX.Element
|
||||
export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick, omitExposedJoins}: FieldListMenuProps): JSX.Element
|
||||
{
|
||||
const [menuAnchorElement, setMenuAnchorElement] = useState(null);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [focusedIndex, setFocusedIndex] = useState(null as number);
|
||||
|
||||
const [fieldsByTable, setFieldsByTable] = useState([] as TableWithFields[]);
|
||||
const [collapsedTables, setCollapsedTables] = useState({} as {[tableName: string]: boolean});
|
||||
const [collapsedTables, setCollapsedTables] = useState({} as { [tableName: string]: boolean });
|
||||
|
||||
const [lastMouseOverXY, setLastMouseOverXY] = useState({x: 0, y: 0});
|
||||
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0)
|
||||
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0);
|
||||
|
||||
const availableExposedJoins = useMemo(() =>
|
||||
{
|
||||
const rs: QExposedJoin[] = []
|
||||
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
|
||||
{
|
||||
if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
rs.push(exposedJoin);
|
||||
}
|
||||
return (rs);
|
||||
}, [tableMetaData, omitExposedJoins]);
|
||||
|
||||
//////////////////
|
||||
// check usages //
|
||||
//////////////////
|
||||
if(isModeSelectOne)
|
||||
if (isModeSelectOne)
|
||||
{
|
||||
if(!handleSelectedField)
|
||||
if (!handleSelectedField)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeSelectOne=true, then a callback for handleSelectedField must be provided.");
|
||||
throw ("In FieldListMenu, if isModeSelectOne=true, then a callback for handleSelectedField must be provided.");
|
||||
}
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
if (isModeToggle)
|
||||
{
|
||||
if(!toggleStates)
|
||||
if (!toggleStates)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeToggle=true, then a model for toggleStates must be provided.");
|
||||
throw ("In FieldListMenu, if isModeToggle=true, then a model for toggleStates must be provided.");
|
||||
}
|
||||
if(!handleToggleField)
|
||||
if (!handleToggleField)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeToggle=true, then a callback for handleToggleField must be provided.");
|
||||
throw ("In FieldListMenu, if isModeToggle=true, then a callback for handleToggleField must be provided.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,16 +126,16 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
{
|
||||
collapsedTables[tableMetaData.name] = false;
|
||||
|
||||
if (tableMetaData.exposedJoins?.length > 0)
|
||||
if (availableExposedJoins?.length > 0)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have exposed joins, put the table meta data with its fields, and then all of the join tables & fields too //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
fieldsByTable.push({table: tableMetaData, fields: getTableFieldsAsAlphabeticalArray(tableMetaData)});
|
||||
|
||||
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++)
|
||||
for (let i = 0; i < availableExposedJoins?.length; i++)
|
||||
{
|
||||
const joinTable = tableMetaData.exposedJoins[i].joinTable;
|
||||
const joinTable = availableExposedJoins[i].joinTable;
|
||||
fieldsByTable.push({table: joinTable, fields: getTableFieldsAsAlphabeticalArray(joinTable)});
|
||||
|
||||
collapsedTables[joinTable.name] = false;
|
||||
@ -150,16 +163,16 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
table.fields.forEach(field =>
|
||||
{
|
||||
let fullFieldName = field.name;
|
||||
if(table.name != tableMetaData.name)
|
||||
if (table.name != tableMetaData.name)
|
||||
{
|
||||
fullFieldName = `${table.name}.${field.name}`;
|
||||
}
|
||||
|
||||
if(fieldNamesToHide && fieldNamesToHide.indexOf(fullFieldName) > -1)
|
||||
if (fieldNamesToHide && fieldNamesToHide.indexOf(fullFieldName) > -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
fields.push(field)
|
||||
fields.push(field);
|
||||
});
|
||||
fields.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return (fields);
|
||||
@ -181,7 +194,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getShownFieldAndTableByIndex(targetIndex: number): {field: QFieldMetaData, table: QTableMetaData}
|
||||
function getShownFieldAndTableByIndex(targetIndex: number): { field: QFieldMetaData, table: QTableMetaData }
|
||||
{
|
||||
let index = -1;
|
||||
for (let i = 0; i < fieldsByTableToShow.length; i++)
|
||||
@ -191,9 +204,9 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
{
|
||||
index++;
|
||||
|
||||
if(index == targetIndex)
|
||||
if (index == targetIndex)
|
||||
{
|
||||
return {field: tableWithField.fields[j], table: tableWithField.table}
|
||||
return {field: tableWithField.fields[j], table: tableWithField.table};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,7 +223,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
// console.log(`Event key: ${event.key}`);
|
||||
setTimeout(() => document.getElementById(`field-list-dropdown-${idPrefix}-textField`).focus());
|
||||
|
||||
if(isModeSelectOne && event.key == "Enter" && focusedIndex != null)
|
||||
if (isModeSelectOne && event.key == "Enter" && focusedIndex != null)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
@ -249,13 +262,13 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
/////////////////
|
||||
// a down move //
|
||||
/////////////////
|
||||
if(startIndex == null)
|
||||
if (startIndex == null)
|
||||
{
|
||||
startIndex = -1;
|
||||
}
|
||||
|
||||
let goalIndex = startIndex + offset;
|
||||
if(goalIndex > maxFieldIndex - 1)
|
||||
if (goalIndex > maxFieldIndex - 1)
|
||||
{
|
||||
goalIndex = maxFieldIndex - 1;
|
||||
}
|
||||
@ -268,7 +281,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
// an up move //
|
||||
////////////////
|
||||
let goalIndex = startIndex + offset;
|
||||
if(goalIndex < 0)
|
||||
if (goalIndex < 0)
|
||||
{
|
||||
goalIndex = 0;
|
||||
}
|
||||
@ -335,7 +348,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
// the last x/y isn't really useful, because the mouse generally isn't left exactly where it was when the mouse-over happened (edge of the element) //
|
||||
// but the keyboard last-arrow time that we capture, that's what's actually being useful in here //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(event.clientX == lastMouseOverXY.x && event.clientY == lastMouseOverXY.y)
|
||||
if (event.clientX == lastMouseOverXY.x && event.clientY == lastMouseOverXY.y)
|
||||
{
|
||||
// console.log("mouse didn't move, so, doesn't count");
|
||||
return;
|
||||
@ -343,7 +356,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
|
||||
const now = new Date().getTime();
|
||||
// console.log(`Compare now [${now}] to last arrow [${timeOfLastArrow}] (diff: [${now - timeOfLastArrow}])`);
|
||||
if(now < timeOfLastArrow + 300)
|
||||
if (now < timeOfLastArrow + 300)
|
||||
{
|
||||
// console.log("An arrow event happened less than 300 mills ago, so doesn't count.");
|
||||
return;
|
||||
@ -480,7 +493,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
for (let i = 0; i < fieldsList.length; i++)
|
||||
{
|
||||
const field = fieldsList[i];
|
||||
if(doesFieldMatchSearchText(field))
|
||||
if (doesFieldMatchSearchText(field))
|
||||
{
|
||||
handleToggleField(field, table, event.target.checked);
|
||||
}
|
||||
@ -491,18 +504,18 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
/////////////////////////////////////////////////////////
|
||||
// compute the table-level toggle state & count values //
|
||||
/////////////////////////////////////////////////////////
|
||||
const tableToggleStates: {[tableName: string]: boolean} = {};
|
||||
const tableToggleCounts: {[tableName: string]: number} = {};
|
||||
const tableToggleStates: { [tableName: string]: boolean } = {};
|
||||
const tableToggleCounts: { [tableName: string]: number } = {};
|
||||
|
||||
if(isModeToggle)
|
||||
if (isModeToggle)
|
||||
{
|
||||
const {allOn, count} = getTableToggleState(tableMetaData, true);
|
||||
tableToggleStates[tableMetaData.name] = allOn;
|
||||
tableToggleCounts[tableMetaData.name] = count;
|
||||
|
||||
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++)
|
||||
for (let i = 0; i < availableExposedJoins?.length; i++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
const join = availableExposedJoins[i];
|
||||
const {allOn, count} = getTableToggleState(join.joinTable, false);
|
||||
tableToggleStates[join.joinTable.name] = allOn;
|
||||
tableToggleCounts[join.joinTable.name] = count;
|
||||
@ -513,7 +526,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getTableToggleState(table: QTableMetaData, isMainTable: boolean): {allOn: boolean, count: number}
|
||||
function getTableToggleState(table: QTableMetaData, isMainTable: boolean): { allOn: boolean, count: number }
|
||||
{
|
||||
const fieldsList = [...table.fields.values()];
|
||||
let allOn = true;
|
||||
@ -522,7 +535,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
{
|
||||
const field = fieldsList[i];
|
||||
const name = isMainTable ? field.name : `${table.name}.${field.name}`;
|
||||
if(!toggleStates[name])
|
||||
if (!toggleStates[name])
|
||||
{
|
||||
allOn = false;
|
||||
}
|
||||
@ -541,7 +554,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
*******************************************************************************/
|
||||
function toggleCollapsedTable(tableName: string)
|
||||
{
|
||||
collapsedTables[tableName] = !collapsedTables[tableName]
|
||||
collapsedTables[tableName] = !collapsedTables[tableName];
|
||||
setCollapsedTables(Object.assign({}, collapsedTables));
|
||||
}
|
||||
|
||||
@ -559,7 +572,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
|
||||
let index = -1;
|
||||
const textFieldId = `field-list-dropdown-${idPrefix}-textField`;
|
||||
let listItemPadding = isModeToggle ? "0.125rem": "0.5rem";
|
||||
let listItemPadding = isModeToggle ? "0.125rem" : "0.5rem";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for z-indexes, we set each table header to i+1, then the fields in that table to i (so they go behind it) //
|
||||
@ -607,12 +620,12 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
{
|
||||
let headerContents = null;
|
||||
const headerTable = tableWithFields.table || tableMetaData;
|
||||
if(tableWithFields.table || showTableHeaderEvenIfNoExposedJoins)
|
||||
if (tableWithFields.table || showTableHeaderEvenIfNoExposedJoins)
|
||||
{
|
||||
headerContents = (<b>{headerTable.label} Fields</b>);
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
if (isModeToggle)
|
||||
{
|
||||
headerContents = (<FormControlLabel
|
||||
sx={{display: "flex", alignItems: "flex-start", "& .MuiFormControlLabel-label": {lineHeight: "1.4", fontWeight: "500 !important"}}}
|
||||
@ -622,10 +635,10 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
checked={tableToggleStates[headerTable.name]}
|
||||
onChange={(event) => handleTableToggle(event, headerTable)}
|
||||
/>}
|
||||
label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b> <span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />)
|
||||
label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b> <span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />);
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
if (isModeToggle)
|
||||
{
|
||||
headerContents = (
|
||||
<>
|
||||
@ -638,11 +651,11 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
</IconButton>
|
||||
{headerContents}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let marginLeft = "unset";
|
||||
if(isModeToggle)
|
||||
if (isModeToggle)
|
||||
{
|
||||
marginLeft = "-1rem";
|
||||
}
|
||||
@ -652,14 +665,14 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
return (
|
||||
<React.Fragment key={tableWithFields.table?.name ?? "theTable"}>
|
||||
<>
|
||||
{headerContents && <ListItem sx={{position: "sticky", top: -1, zIndex: zIndex+1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start", backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 90%, rgba(255,255,255,0))"}}>{headerContents}</ListItem>}
|
||||
{headerContents && <ListItem sx={{position: "sticky", top: -1, zIndex: zIndex + 1, padding: listItemPadding, ml: marginLeft, display: "flex", alignItems: "flex-start", backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 90%, rgba(255,255,255,0))"}}>{headerContents}</ListItem>}
|
||||
{
|
||||
tableWithFields.fields.map((field) =>
|
||||
{
|
||||
index++;
|
||||
const key = `${tableWithFields.table?.name}-${field.name}`
|
||||
const key = `${tableWithFields.table?.name}-${field.name}`;
|
||||
|
||||
if(collapsedTables[headerTable.name])
|
||||
if (collapsedTables[headerTable.name])
|
||||
{
|
||||
return (<React.Fragment key={key} />);
|
||||
}
|
||||
@ -677,13 +690,13 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
|
||||
{
|
||||
closeMenu();
|
||||
handleSelectedField(field, tableWithFields.table ?? tableMetaData);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let label: JSX.Element | string = field.label;
|
||||
const fullFieldName = tableWithFields.table && tableWithFields.table.name != tableMetaData.name ? `${tableWithFields.table.name}.${field.name}` : field.name;
|
||||
|
||||
if(fieldEndAdornment)
|
||||
if (fieldEndAdornment)
|
||||
{
|
||||
label = <Box width="100%" display="inline-flex" justifyContent="space-between">
|
||||
{label}
|
||||
|
@ -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,152 +318,207 @@ 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 + " 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>
|
||||
<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>
|
||||
{
|
||||
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
|
||||
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>
|
||||
</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>
|
||||
|
@ -33,6 +33,7 @@ import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {omit} from "lodash";
|
||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
||||
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
||||
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
||||
@ -109,6 +110,7 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
|
||||
{
|
||||
case QFieldType.DECIMAL:
|
||||
case QFieldType.INTEGER:
|
||||
case QFieldType.LONG:
|
||||
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
||||
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
|
||||
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
|
||||
@ -189,17 +191,18 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
|
||||
|
||||
interface FilterCriteriaRowProps
|
||||
{
|
||||
id: number;
|
||||
index: number;
|
||||
tableMetaData: QTableMetaData;
|
||||
metaData: QInstance;
|
||||
criteria: QFilterCriteria;
|
||||
booleanOperator: "AND" | "OR" | null;
|
||||
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
|
||||
removeCriteria: () => void;
|
||||
updateBooleanOperator: (newValue: string) => void;
|
||||
queryScreenUsage?: QueryScreenUsage;
|
||||
allowVariables?: boolean;
|
||||
id: number,
|
||||
index: number,
|
||||
tableMetaData: QTableMetaData,
|
||||
metaData: QInstance,
|
||||
criteria: QFilterCriteria,
|
||||
booleanOperator: "AND" | "OR" | null,
|
||||
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void,
|
||||
removeCriteria: () => void,
|
||||
updateBooleanOperator: (newValue: string) => void,
|
||||
queryScreenUsage?: QueryScreenUsage,
|
||||
allowVariables?: boolean,
|
||||
omitExposedJoins?: string[]
|
||||
}
|
||||
|
||||
FilterCriteriaRow.defaultProps =
|
||||
@ -268,7 +271,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
|
||||
return {criteriaIsValid, criteriaStatusTooltip};
|
||||
}
|
||||
|
||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element
|
||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables, omitExposedJoins}: FilterCriteriaRowProps): JSX.Element
|
||||
{
|
||||
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
||||
@ -487,7 +490,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
</Box>
|
||||
<Box display="inline-block" width={250} className="fieldColumn">
|
||||
<FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange}
|
||||
autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
||||
omitExposedJoins={omitExposedJoins} autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="inline-block" width={200} className="operatorColumn">
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -29,9 +29,9 @@ import Icon from "@mui/material/Icon";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||
|
||||
interface QueryScreenActionMenuProps
|
||||
{
|
||||
@ -44,40 +44,35 @@ interface QueryScreenActionMenuProps
|
||||
processClicked: (process: QProcessMetaData) => void;
|
||||
}
|
||||
|
||||
QueryScreenActionMenu.defaultProps = {
|
||||
};
|
||||
QueryScreenActionMenu.defaultProps = {};
|
||||
|
||||
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
|
||||
{
|
||||
const [anchorElement, setAnchorElement] = useState(null)
|
||||
const [anchorElement, setAnchorElement] = useState(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openActionsMenu = (event: any) =>
|
||||
{
|
||||
setAnchorElement(event.currentTarget);
|
||||
}
|
||||
};
|
||||
|
||||
const closeActionsMenu = () =>
|
||||
{
|
||||
setAnchorElement(null);
|
||||
}
|
||||
|
||||
const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
|
||||
{
|
||||
if (menuItems.length > 0)
|
||||
{
|
||||
menuItems.push(<Divider key="divider" />);
|
||||
}
|
||||
};
|
||||
|
||||
const runSomething = (handler: () => void) =>
|
||||
{
|
||||
closeActionsMenu();
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems: JSX.Element[] = [];
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// start with bulk actions, if user has permissions //
|
||||
//////////////////////////////////////////////////////
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkLoad" onClick={() => runSomething(bulkLoadClicked)}><ListItemIcon><Icon>library_add</Icon></ListItemIcon>Bulk Load</MenuItem>);
|
||||
@ -91,19 +86,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
menuItems.push(<MenuItem key="bulkDelete" onClick={() => runSomething(bulkDeleteClicked)}><ListItemIcon><Icon>delete</Icon></ListItemIcon>Bulk Delete</MenuItem>);
|
||||
}
|
||||
|
||||
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||
if (runRecordScriptProcess)
|
||||
{
|
||||
const process = runRecordScriptProcess;
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
}
|
||||
|
||||
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||
|
||||
if (tableProcesses && tableProcesses.length)
|
||||
{
|
||||
pushDividerIfNeeded(menuItems);
|
||||
}
|
||||
menuItems.push(<Divider key="divider1" />);
|
||||
|
||||
tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
|
||||
tableProcesses.map((process) =>
|
||||
@ -111,11 +94,62 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
});
|
||||
|
||||
menuItems.push(<Divider key="divider2" />);
|
||||
|
||||
////////////////////////////////////////////
|
||||
// add processes that apply to all tables //
|
||||
////////////////////////////////////////////
|
||||
const materialDashboardInstanceMetaData = metaData.supplementalInstanceMetaData?.get("materialDashboard");
|
||||
if (materialDashboardInstanceMetaData)
|
||||
{
|
||||
const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
|
||||
if (processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
for (let processName of processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
const process = metaData?.processes.get(processName);
|
||||
if (process)
|
||||
{
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////
|
||||
// deprecated in favor of the above //
|
||||
//////////////////////////////////////
|
||||
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||
if (runRecordScriptProcess)
|
||||
{
|
||||
const process = runRecordScriptProcess;
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// todo - any conditions around this? //
|
||||
////////////////////////////////////////
|
||||
menuItems.push(<MenuItem key="developerMode" onClick={() => navigate(`${metaData.getTablePathByName(tableMetaData.name)}/dev`)}><ListItemIcon><Icon>code</Icon></ListItemIcon>Developer Mode</MenuItem>);
|
||||
|
||||
if (menuItems.length === 0)
|
||||
{
|
||||
menuItems.push(<MenuItem key="notAvaialableNow" disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// remove any duplicated dividers, and any dividers in the first or last slot //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
for (let i = 0; i < menuItems.length; i++)
|
||||
{
|
||||
if (menuItems[i].type == Divider && (i == 0 || (i > 0 && menuItems[i - 1].type == Divider) || i == menuItems.length - 1))
|
||||
{
|
||||
menuItems.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<QActionsMenuButton isOpen={anchorElement} onClickHandler={openActionsMenu} />
|
||||
@ -130,5 +164,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
||||
{menuItems}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
@ -118,7 +118,7 @@ const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteri
|
||||
** autocomplete), given an array of options, the query's active criteria in this
|
||||
** field, and the default operator to use for this field
|
||||
*******************************************************************************/
|
||||
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption =>
|
||||
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator, return0thOptionInsteadOfNull: boolean = false): OperatorOption =>
|
||||
{
|
||||
if (criteria)
|
||||
{
|
||||
@ -135,6 +135,23 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
||||
return (filteredOptions[0]);
|
||||
}
|
||||
|
||||
if (return0thOptionInsteadOfNull)
|
||||
{
|
||||
console.log("Returning 0th operator instead of null - this isn't expected, but has been seen to happen - so here's some additional debugging:");
|
||||
try
|
||||
{
|
||||
console.log("Operator options: " + JSON.stringify(operatorOptions));
|
||||
console.log("Criteria: " + JSON.stringify(criteria));
|
||||
console.log("Default Operator: " + JSON.stringify(defaultOperator));
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error in debug output: ${e}`);
|
||||
}
|
||||
|
||||
return operatorOptions[0];
|
||||
}
|
||||
|
||||
return (null);
|
||||
};
|
||||
|
||||
@ -157,7 +174,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? Object.assign({}, criteriaParam) as QFilterCriteriaWithId : null);
|
||||
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
||||
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator, true));
|
||||
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
||||
|
||||
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
||||
@ -169,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)]);
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -49,7 +49,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
||||
<Card sx={{width: "100%", height: "100%"}}>
|
||||
<Typography variant="h6" p={2} pb={1}>{heading}</Typography>
|
||||
<Box className="devDocumentation" height="100%">
|
||||
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto", height: "100%"}}>
|
||||
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto", height: "calc(100% - 0.5rem)"}}>
|
||||
<AceEditor
|
||||
mode={mode}
|
||||
theme="github"
|
||||
@ -62,7 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
||||
width="100%"
|
||||
showPrintMargin={false}
|
||||
height="100%"
|
||||
style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}}
|
||||
style={{borderBottomRightRadius: "0.75rem", borderBottomLeftRadius: "0.75rem"}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
@ -39,6 +39,7 @@ import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineCha
|
||||
import PieChart from "qqq/components/widgets/charts/piechart/PieChart";
|
||||
import StackedBarChart from "qqq/components/widgets/charts/StackedBarChart";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import CustomComponentWidget from "qqq/components/widgets/misc/CustomComponentWidget";
|
||||
import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
||||
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
||||
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
||||
@ -313,6 +314,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
|
||||
{
|
||||
updateChildRecordList(name, "delete", rowIndex);
|
||||
forceUpdate();
|
||||
actionCallback(widgetData[widgetIndex]);
|
||||
};
|
||||
|
||||
@ -368,7 +370,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function submitEditChildForm(values: any)
|
||||
function submitEditChildForm(values: any, tableName: string)
|
||||
{
|
||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
||||
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
|
||||
@ -718,6 +720,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
addNewRecordCallback={widgetData[i]?.isInProcess ? () => openAddChildRecord(widgetMetaData.name, widgetData[i]) : null}
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
parentRecord={record}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -779,8 +782,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
{
|
||||
widgetMetaData.type === "filterAndColumnsSetup" && (
|
||||
widgetData && widgetData[i] &&
|
||||
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
||||
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={(values: { [name: string]: any }) =>
|
||||
{
|
||||
if(actionCallback)
|
||||
{
|
||||
actionCallback(values)
|
||||
}
|
||||
}} />
|
||||
)
|
||||
}
|
||||
@ -798,6 +805,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
<DynamicFormWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} recordValues={convertQRecordValuesFromMapToObject(record)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "customComponent" && (
|
||||
widgetData && widgetData[i] &&
|
||||
<Widget widgetMetaData={widgetMetaData}>
|
||||
<CustomComponentWidget widgetMetaData={widgetMetaData} widgetData={widgetData[i]} record={record} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -728,7 +728,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||
{
|
||||
needLabelBox &&
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%", ...props.labelBoxAdditionalSx}} minHeight={"2.5rem"} className="widgetLabelBox">
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box display="flex" alignItems="baseline">
|
||||
{
|
||||
|
69
src/qqq/components/widgets/misc/CustomComponentWidget.tsx
Normal file
69
src/qqq/components/widgets/misc/CustomComponentWidget.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import useDynamicComponents from "qqq/utils/qqq/useDynamicComponents";
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
|
||||
interface CustomComponentWidgetProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
widgetData: any;
|
||||
record: QRecord;
|
||||
}
|
||||
|
||||
|
||||
CustomComponentWidget.defaultProps = {
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to display a custom component - one dynamically loaded.
|
||||
*******************************************************************************/
|
||||
export default function CustomComponentWidget({widgetMetaData, widgetData, record}: CustomComponentWidgetProps): JSX.Element
|
||||
{
|
||||
const [componentName, setComponentName] = useState(widgetMetaData.defaultValues.get("componentName"));
|
||||
const [componentSourceUrl, setComponentSourceUrl] = useState(widgetMetaData.defaultValues.get("componentSourceUrl"));
|
||||
|
||||
const {loadComponent, hasComponentLoaded, renderComponent} = useDynamicComponents();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
loadComponent(componentName, componentSourceUrl);
|
||||
}, []);
|
||||
|
||||
const props: any =
|
||||
{
|
||||
widgetMetaData: widgetMetaData,
|
||||
widgetData: widgetData,
|
||||
record: record,
|
||||
}
|
||||
|
||||
return (<Box sx={widgetMetaData.defaultValues?.get("sx")}>
|
||||
{hasComponentLoaded(componentName) ? renderComponent(componentName, props) : <Skeleton width="100%" height="100%" />}
|
||||
</Box>);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
@ -42,15 +43,17 @@ import QQueryColumns, {Column} from "qqq/models/query/QQueryColumns";
|
||||
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
|
||||
|
||||
interface FilterAndColumnsSetupWidgetProps
|
||||
{
|
||||
isEditable: boolean;
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
widgetData: any;
|
||||
recordValues: { [name: string]: any };
|
||||
onSaveCallback?: (values: { [name: string]: any }) => void;
|
||||
isEditable: boolean,
|
||||
widgetMetaData: QWidgetMetaData,
|
||||
widgetData: any,
|
||||
recordValues: { [name: string]: any },
|
||||
onSaveCallback?: (values: { [name: string]: any }) => void,
|
||||
label?: string
|
||||
}
|
||||
|
||||
FilterAndColumnsSetupWidget.defaultProps = {
|
||||
@ -79,18 +82,31 @@ unborderedButtonSX.opacity = "0.7";
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
const qControllerV1 = Client.getInstanceV1();
|
||||
|
||||
/*******************************************************************************
|
||||
** Component for editing the main setup of a report - that is: filter & columns
|
||||
*******************************************************************************/
|
||||
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||
export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||
{
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
||||
const [hideColumns] = useState(widgetData?.hideColumns);
|
||||
const [hidePreview] = useState(widgetData?.hidePreview);
|
||||
const [hideSortBy] = useState(widgetData?.hideSortBy);
|
||||
const [isEditable] = useState(widgetData?.overrideIsEditable ?? isEditableProp);
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
|
||||
const [isApiVersioned] = useState(widgetData?.isApiVersioned);
|
||||
const [apiVersion, setApiVersion] = useState(null as ApiVersion | null);
|
||||
|
||||
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
|
||||
const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson");
|
||||
|
||||
const [alertContent, setAlertContent] = useState(null as string);
|
||||
const [warning, setWarning] = useState(null as string);
|
||||
const [widgetFailureAlertContent, setWidgetFailureAlertContent] = useState(null as string);
|
||||
|
||||
const omitExposedJoins: string[] = widgetData?.omitExposedJoins ?? [];
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we'll actually keep 2 copies of the query filter around here - //
|
||||
@ -108,7 +124,9 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
/////////////////////////////
|
||||
let columns: QQueryColumns = null;
|
||||
let usingDefaultEmptyFilter = false;
|
||||
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
||||
const rawFilterValueFromRecord = recordValues[filterFieldName];
|
||||
let queryFilter = rawFilterValueFromRecord &&
|
||||
((typeof rawFilterValueFromRecord == "string" ? JSON.parse(rawFilterValueFromRecord) : rawFilterValueFromRecord) as QQueryFilter);
|
||||
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
|
||||
if (!queryFilter)
|
||||
{
|
||||
@ -142,9 +160,9 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
});
|
||||
}
|
||||
|
||||
if (recordValues["columnsJson"])
|
||||
if (recordValues[columnsFieldName])
|
||||
{
|
||||
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
|
||||
columns = QQueryColumns.buildFromJSON(recordValues[columnsFieldName]);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@ -161,16 +179,73 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
tableName = recordValues["tableName"];
|
||||
}
|
||||
|
||||
let version: ApiVersion | null = null;
|
||||
if (isApiVersioned)
|
||||
{
|
||||
let apiName = widgetData?.apiName;
|
||||
let apiPath = widgetData?.apiPath;
|
||||
let apiVersion = widgetData?.apiVersion;
|
||||
|
||||
if (!apiName && recordValues["apiName"])
|
||||
{
|
||||
apiName = recordValues["apiName"];
|
||||
}
|
||||
|
||||
if (!apiPath && recordValues["apiPath"])
|
||||
{
|
||||
apiPath = recordValues["apiPath"];
|
||||
}
|
||||
|
||||
if (!apiVersion && recordValues["apiVersion"])
|
||||
{
|
||||
apiVersion = recordValues["apiVersion"];
|
||||
}
|
||||
|
||||
if (!apiName || !apiPath || !apiVersion)
|
||||
{
|
||||
console.log("API Name/Path/Version not set, but widget isApiVersioned, so cannot load table meta data...");
|
||||
return;
|
||||
}
|
||||
|
||||
version = {name: apiName, path: apiPath, version: apiVersion};
|
||||
setApiVersion(version);
|
||||
}
|
||||
|
||||
if (tableName)
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
try
|
||||
{
|
||||
const tableMetaData = await qControllerV1.loadTableMetaData(tableName, version);
|
||||
setTableMetaData(tableMetaData);
|
||||
|
||||
const queryFilterForFrontend = Object.assign({}, queryFilter);
|
||||
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
|
||||
setFrontendQueryFilter(queryFilterForFrontend);
|
||||
const queryFilterForFrontend = Object.assign({}, queryFilter);
|
||||
|
||||
let warnings: string[] = [];
|
||||
for (let i = 0; i < queryFilterForFrontend?.criteria?.length; i++)
|
||||
{
|
||||
const criteria = queryFilter.criteria[i];
|
||||
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
|
||||
if(!field)
|
||||
{
|
||||
warnings.push("Removing non-existing filter field: " + criteria.fieldName);
|
||||
queryFilterForFrontend.criteria.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend);
|
||||
setFrontendQueryFilter(queryFilterForFrontend);
|
||||
|
||||
setWarning(warnings.join("; "));
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(e);
|
||||
//@ts-ignore e.message
|
||||
setWidgetFailureAlertContent("Error preparing filter widget: " + (e.message ?? "Details not available."));
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [JSON.stringify(recordValues)]);
|
||||
@ -199,7 +274,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordValues["tableName"])
|
||||
if (widgetData?.tableName || recordValues["tableName"])
|
||||
{
|
||||
setAlertContent(null);
|
||||
setModalOpen(true);
|
||||
@ -230,7 +305,10 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
setFrontendQueryFilter(view.queryFilter);
|
||||
const filter = FilterUtils.prepQueryFilterForBackend(tableMetaData, view.queryFilter);
|
||||
|
||||
onSaveCallback({queryFilterJson: JSON.stringify(filter), columnsJson: JSON.stringify(view.queryColumns)});
|
||||
const rs: { [key: string]: any } = {};
|
||||
rs[filterFieldName] = JSON.stringify(filter);
|
||||
rs[columnsFieldName] = JSON.stringify(view.queryColumns);
|
||||
onSaveCallback(rs);
|
||||
|
||||
closeEditor();
|
||||
}
|
||||
@ -328,7 +406,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
/////////////////////////////////////////////////
|
||||
// add link to widget header for opening modal //
|
||||
/////////////////////////////////////////////////
|
||||
const selectTableFirstTooltipTitle = tableMetaData ? null : "You must select a table before you can set up your report filters and columns";
|
||||
const selectTableFirstTooltipTitle = tableMetaData ? null : `You must select a table${isApiVersioned ? " and API details" : ""} before you can set up your filters${hideColumns ? "" : " and columns"}`;
|
||||
const labelAdditionalElementsRight: JSX.Element[] = [];
|
||||
if (isEditable)
|
||||
{
|
||||
@ -342,6 +420,12 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
}
|
||||
}
|
||||
|
||||
if (widgetFailureAlertContent)
|
||||
{
|
||||
return (<Widget widgetMetaData={widgetMetaData}>
|
||||
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}}>{widgetFailureAlertContent}</Alert>
|
||||
</Widget>);
|
||||
}
|
||||
|
||||
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}>
|
||||
<React.Fragment>
|
||||
@ -354,10 +438,13 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<Collapse in={Boolean(alertContent)}>
|
||||
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||
</Collapse>
|
||||
<Collapse in={Boolean(warning)}>
|
||||
<Alert severity="warning" sx={{mt: 1.5, mb: 0.5}} onClose={() => setWarning(null)}>{warning}</Alert>
|
||||
</Collapse>
|
||||
<Box pt="0.5rem">
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<h5>Query Filter</h5>
|
||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||
<h5>{label ?? widgetData.label ?? widgetMetaData.label ?? "Query Filter"}</h5>
|
||||
{!hideSortBy && <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>}
|
||||
</Box>
|
||||
{
|
||||
mayShowQuery() &&
|
||||
@ -369,7 +456,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
{
|
||||
isEditable &&
|
||||
<Tooltip title={selectTableFirstTooltipTitle}>
|
||||
<span><Button disabled={!recordValues["tableName"]} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span>
|
||||
<span><Button disabled={tableMetaData == null} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
@ -415,6 +502,8 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
isModal={true}
|
||||
initialQueryFilter={frontendQueryFilter}
|
||||
initialColumns={columns}
|
||||
apiVersion={apiVersion}
|
||||
omitExposedJoins={omitExposedJoins}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@ -424,7 +513,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<div>
|
||||
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
|
||||
<Card sx={{m: "2rem", p: "2rem"}}>
|
||||
<h3>Edit Filters and Columns</h3>
|
||||
<h3>Edit Filters {hideColumns ? "" : " and Columns"}</h3>
|
||||
{
|
||||
showHelp("modalSubheader") &&
|
||||
<Box color={colors.gray.main} pb={"0.5rem"}>
|
||||
@ -440,6 +529,8 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
isModal={true}
|
||||
initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter}
|
||||
initialColumns={columns}
|
||||
apiVersion={apiVersion}
|
||||
omitExposedJoins={omitExposedJoins}
|
||||
/>
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ import {Link, useNavigate} from "react-router-dom";
|
||||
export interface ChildRecordListData extends WidgetData
|
||||
{
|
||||
title?: string;
|
||||
queryOutput?: { records: { values: any }[] };
|
||||
queryOutput?: { records: { values: any, displayValues?: any } [] };
|
||||
childTableMetaData?: QTableMetaData;
|
||||
tablePath?: string;
|
||||
viewAllLink?: string;
|
||||
@ -48,20 +48,23 @@ export interface ChildRecordListData extends WidgetData
|
||||
canAddChildRecord?: boolean;
|
||||
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
|
||||
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
|
||||
defaultValuesForNewChildRecordsFromParentFields?: { [fieldName: string]: string };
|
||||
omitFieldNames?: string[];
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: ChildRecordListData;
|
||||
addNewRecordCallback?: () => void;
|
||||
disableRowClick: boolean;
|
||||
allowRecordEdit: boolean;
|
||||
editRecordCallback?: (rowIndex: number) => void;
|
||||
allowRecordDelete: boolean;
|
||||
deleteRecordCallback?: (rowIndex: number) => void;
|
||||
gridOnly?: boolean;
|
||||
gridDensity?: GridDensity;
|
||||
widgetMetaData: QWidgetMetaData,
|
||||
data: ChildRecordListData,
|
||||
addNewRecordCallback?: () => void,
|
||||
disableRowClick: boolean,
|
||||
allowRecordEdit: boolean,
|
||||
editRecordCallback?: (rowIndex: number) => void,
|
||||
allowRecordDelete: boolean,
|
||||
deleteRecordCallback?: (rowIndex: number) => void,
|
||||
gridOnly?: boolean,
|
||||
gridDensity?: GridDensity,
|
||||
parentRecord?: QRecord
|
||||
}
|
||||
|
||||
RecordGridWidget.defaultProps =
|
||||
@ -74,7 +77,7 @@ RecordGridWidget.defaultProps =
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback, gridOnly, gridDensity}: Props): JSX.Element
|
||||
function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback, gridOnly, gridDensity, parentRecord}: Props): JSX.Element
|
||||
{
|
||||
const instance = useRef({timer: null});
|
||||
const [rows, setRows] = useState([]);
|
||||
@ -97,7 +100,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
{
|
||||
for (let i = 0; i < queryOutputRecords.length; i++)
|
||||
{
|
||||
if(queryOutputRecords[i] instanceof QRecord)
|
||||
if (queryOutputRecords[i] instanceof QRecord)
|
||||
{
|
||||
records.push(queryOutputRecords[i] as QRecord);
|
||||
}
|
||||
@ -109,7 +112,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
}
|
||||
|
||||
const tableMetaData = data.childTableMetaData instanceof QTableMetaData ? data.childTableMetaData as QTableMetaData : new QTableMetaData(data.childTableMetaData);
|
||||
const rows = DataGridUtils.makeRows(records, tableMetaData, true);
|
||||
const rows = DataGridUtils.makeRows(records, tableMetaData, undefined, true);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// note - tablePath may be null, if the user doesn't have access to the table. //
|
||||
@ -117,6 +120,19 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath;
|
||||
const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection");
|
||||
|
||||
if (data.omitFieldNames)
|
||||
{
|
||||
for (let i = 0; i < columns.length; i++)
|
||||
{
|
||||
const column = columns[i];
|
||||
if (data.omitFieldNames.indexOf(column.field) > -1)
|
||||
{
|
||||
columns.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -252,7 +268,22 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
{
|
||||
disabledFields = data.defaultValuesForNewChildRecords;
|
||||
}
|
||||
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
|
||||
|
||||
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy values from specified fields in the parent record down into the child record //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if (data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
for (let childField in data.defaultValuesForNewChildRecordsFromParentFields)
|
||||
{
|
||||
const parentField = data.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||
defaultValuesForNewChildRecords[childField] = parentRecord?.values?.get(parentField);
|
||||
}
|
||||
}
|
||||
|
||||
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
|
||||
}
|
||||
|
||||
|
||||
@ -357,7 +388,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
/>
|
||||
);
|
||||
|
||||
if(gridOnly)
|
||||
if (gridOnly)
|
||||
{
|
||||
return (grid);
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container className="scriptViewer" m={-2} pt={4} width={"calc(100% + 2rem)"}>
|
||||
<Grid container className="scriptViewer" my={-3} mx={-3} pt={4} width={"calc(100% + 3rem)"}>
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
{
|
||||
@ -530,7 +530,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel index={2} value={selectedTab}>
|
||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||
<Box sx={{height: "455px"}} px={2} pt={1}>
|
||||
<ScriptTestForm scriptId={scriptId}
|
||||
scriptType={scriptTypeRecord}
|
||||
tableName={associatedScriptTableName}
|
||||
@ -543,7 +543,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel index={3} value={selectedTab}>
|
||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
||||
<Box sx={{height: "455px"}} px={2} pt={1}>
|
||||
<ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
@ -35,7 +35,7 @@ export interface ModalEditFormData
|
||||
defaultValues?: { [key: string]: string };
|
||||
disabledFields?: { [key: string]: boolean } | string[];
|
||||
overrideHeading?: string;
|
||||
onSubmitCallback?: (values: any) => void;
|
||||
onSubmitCallback?: (values: any, tableName: String) => void;
|
||||
initialShowModalValue?: boolean;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
|
||||
/*******************************************************************************
|
||||
** Properties attached to a (formik?) form field, to denote how it behaves as
|
||||
@ -34,5 +35,6 @@ export interface FieldPossibleValueProps
|
||||
tableName?: string;
|
||||
processName?: string;
|
||||
possibleValueSourceName?: string;
|
||||
possibleValueSourceFilter?: QQueryFilter;
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
|
||||
export type ValueType = "defaultValue" | "column";
|
||||
@ -42,6 +43,7 @@ export class BulkLoadField
|
||||
wideLayoutIndexPath: number[] = [];
|
||||
|
||||
error: string = null;
|
||||
warning: string = null;
|
||||
|
||||
key: string;
|
||||
|
||||
@ -49,7 +51,7 @@ export class BulkLoadField
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [])
|
||||
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
|
||||
{
|
||||
this.field = field;
|
||||
this.tableStructure = tableStructure;
|
||||
@ -59,6 +61,8 @@ export class BulkLoadField
|
||||
this.defaultValue = defaultValue;
|
||||
this.doValueMapping = doValueMapping;
|
||||
this.wideLayoutIndexPath = wideLayoutIndexPath;
|
||||
this.error = error;
|
||||
this.warning = warning;
|
||||
this.key = new Date().getTime().toString();
|
||||
}
|
||||
|
||||
@ -68,7 +72,7 @@ export class BulkLoadField
|
||||
***************************************************************************/
|
||||
public static clone(source: BulkLoadField): BulkLoadField
|
||||
{
|
||||
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath));
|
||||
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning));
|
||||
}
|
||||
|
||||
|
||||
@ -422,17 +426,22 @@ export class BulkLoadMapping
|
||||
}
|
||||
else
|
||||
{
|
||||
index = 0;
|
||||
///////////////////////////////////////////////////////////
|
||||
// count how many copies of this field there are already //
|
||||
///////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////
|
||||
// find the max index for this field already //
|
||||
///////////////////////////////////////////////
|
||||
let maxIndex = -1;
|
||||
for (let existingField of [...this.requiredFields, ...this.additionalFields])
|
||||
{
|
||||
if (existingField.getQualifiedName() == bulkLoadField.getQualifiedName())
|
||||
{
|
||||
index++;
|
||||
const thisIndex = existingField.wideLayoutIndexPath[0];
|
||||
if (thisIndex != null && thisIndex != undefined && thisIndex > maxIndex)
|
||||
{
|
||||
maxIndex = thisIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
index = maxIndex + 1;
|
||||
}
|
||||
|
||||
const cloneField = BulkLoadField.clone(bulkLoadField);
|
||||
@ -455,7 +464,7 @@ export class BulkLoadMapping
|
||||
const newAdditionalFields: BulkLoadField[] = [];
|
||||
for (let bulkLoadField of this.additionalFields)
|
||||
{
|
||||
if (bulkLoadField.getQualifiedName() != toRemove.getQualifiedName())
|
||||
if (bulkLoadField.getQualifiedNameWithWideSuffix() != toRemove.getQualifiedNameWithWideSuffix())
|
||||
{
|
||||
newAdditionalFields.push(bulkLoadField);
|
||||
}
|
||||
@ -463,6 +472,171 @@ export class BulkLoadMapping
|
||||
|
||||
this.additionalFields = newAdditionalFields;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public switchLayout(newLayout: string): void
|
||||
{
|
||||
const newAdditionalFields: BulkLoadField[] = [];
|
||||
let anyChanges = false;
|
||||
|
||||
if ("WIDE" != newLayout)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if going to a layout other than WIDE, make sure there aren't any fields with a wideLayoutIndexPath //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const namesWhereOneWideLayoutIndexHasBeenFound: { [name: string]: boolean } = {};
|
||||
for (let existingField of this.additionalFields)
|
||||
{
|
||||
if (existingField.wideLayoutIndexPath.length > 0)
|
||||
{
|
||||
const name = existingField.getQualifiedName();
|
||||
if (namesWhereOneWideLayoutIndexHasBeenFound[name])
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in this case, we're on like the 2nd or 3rd instance of, say, Line Item: SKU - so - just discard it. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
anyChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, this is the 1st instance of, say, Line Item: SKU - so mark that we've found it - and keep this field //
|
||||
// (that is, put it in the new array), but with no index path //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
namesWhereOneWideLayoutIndexHasBeenFound[name] = true;
|
||||
const newField = BulkLoadField.clone(existingField);
|
||||
newField.wideLayoutIndexPath = [];
|
||||
newAdditionalFields.push(newField);
|
||||
anyChanges = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// else, non-wide-path fields, just get added as-is //
|
||||
//////////////////////////////////////////////////////
|
||||
newAdditionalFields.push(existingField);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if going to WIDE layout, then any field from a child table needs a wide-layout-index-path //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for (let existingField of this.additionalFields)
|
||||
{
|
||||
if (existingField.tableStructure.isMain)
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// fields from main table come over as-is //
|
||||
////////////////////////////////////////////
|
||||
newAdditionalFields.push(existingField);
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// fields from child tables get a wideLayoutIndexPath (and we're assuming just 1 for each) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const newField = BulkLoadField.clone(existingField);
|
||||
newField.wideLayoutIndexPath = [0];
|
||||
newAdditionalFields.push(newField);
|
||||
anyChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanges)
|
||||
{
|
||||
this.additionalFields = newAdditionalFields;
|
||||
}
|
||||
|
||||
this.layout = newLayout;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public getFieldsForColumnIndex(i: number): BulkLoadField[]
|
||||
{
|
||||
const rs: BulkLoadField[] = [];
|
||||
|
||||
for (let field of [...this.requiredFields, ...this.additionalFields])
|
||||
{
|
||||
if (field.valueType == "column" && field.columnIndex == i)
|
||||
{
|
||||
rs.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public handleChangeToHasHeaderRow(newValue: any, fileDescription: FileDescription)
|
||||
{
|
||||
const newRequiredFields: BulkLoadField[] = [];
|
||||
let anyChangesToRequiredFields = false;
|
||||
|
||||
const newAdditionalFields: BulkLoadField[] = [];
|
||||
let anyChangesToAdditionalFields = false;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're switching to have header-rows enabled, then make sure that no columns w/ duplicated headers are selected //
|
||||
// strategy to do this: build new lists of both required & additional fields - and track if we had to change any //
|
||||
// column indexes (set to null) - add a warning to them, and only replace the arrays if there were changes. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (newValue)
|
||||
{
|
||||
for (let field of this.requiredFields)
|
||||
{
|
||||
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
||||
{
|
||||
const newField = BulkLoadField.clone(field);
|
||||
newField.columnIndex = null;
|
||||
newField.warning = "This field was assigned to a column with a duplicated header"
|
||||
newRequiredFields.push(newField);
|
||||
anyChangesToRequiredFields = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newRequiredFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
for (let field of this.additionalFields)
|
||||
{
|
||||
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
||||
{
|
||||
const newField = BulkLoadField.clone(field);
|
||||
newField.columnIndex = null;
|
||||
newField.warning = "This field was assigned to a column with a duplicated header"
|
||||
newAdditionalFields.push(newField);
|
||||
anyChangesToAdditionalFields = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newAdditionalFields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChangesToRequiredFields)
|
||||
{
|
||||
this.requiredFields = newRequiredFields;
|
||||
}
|
||||
|
||||
if (anyChangesToAdditionalFields)
|
||||
{
|
||||
this.additionalFields = newAdditionalFields;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -475,6 +649,8 @@ export class FileDescription
|
||||
headerLetters: string[];
|
||||
bodyValuesPreview: string[][];
|
||||
|
||||
duplicateHeaderIndexes: boolean[];
|
||||
|
||||
// todo - just get this from the profile always - it's not part of the file per-se
|
||||
hasHeaderRow: boolean = true;
|
||||
|
||||
@ -486,6 +662,18 @@ export class FileDescription
|
||||
this.headerValues = headerValues;
|
||||
this.headerLetters = headerLetters;
|
||||
this.bodyValuesPreview = bodyValuesPreview;
|
||||
|
||||
this.duplicateHeaderIndexes = [];
|
||||
const usedLabels: { [label: string]: boolean } = {};
|
||||
for (let i = 0; i < headerValues.length; i++)
|
||||
{
|
||||
const label = headerValues[i];
|
||||
if (usedLabels[label])
|
||||
{
|
||||
this.duplicateHeaderIndexes[i] = true;
|
||||
}
|
||||
usedLabels[label] = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -517,21 +705,85 @@ export class FileDescription
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public getPreviewValues(columnIndex: number): string[]
|
||||
public getPreviewValues(columnIndex: number, fieldType?: QFieldType): string[]
|
||||
{
|
||||
if (columnIndex == undefined)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.hasHeaderRow)
|
||||
function getTypedValue(value: any): string
|
||||
{
|
||||
return (this.bodyValuesPreview[columnIndex]);
|
||||
if (value == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this was useful at one point in time when we had an object coming back for xlsx files with many different data types //
|
||||
// we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (value && value.string)
|
||||
{
|
||||
switch (fieldType)
|
||||
{
|
||||
case QFieldType.BOOLEAN:
|
||||
{
|
||||
return value.bool;
|
||||
}
|
||||
|
||||
case QFieldType.STRING:
|
||||
case QFieldType.TEXT:
|
||||
case QFieldType.HTML:
|
||||
case QFieldType.PASSWORD:
|
||||
{
|
||||
return value.string;
|
||||
}
|
||||
|
||||
case QFieldType.INTEGER:
|
||||
case QFieldType.LONG:
|
||||
{
|
||||
return value.integer;
|
||||
}
|
||||
case QFieldType.DECIMAL:
|
||||
{
|
||||
return value.decimal;
|
||||
}
|
||||
case QFieldType.DATE:
|
||||
{
|
||||
return value.date;
|
||||
}
|
||||
case QFieldType.TIME:
|
||||
{
|
||||
return value.time;
|
||||
}
|
||||
case QFieldType.DATE_TIME:
|
||||
{
|
||||
return value.dateTime;
|
||||
}
|
||||
case QFieldType.BLOB:
|
||||
return ""; // !!
|
||||
}
|
||||
}
|
||||
|
||||
return (`${value}`);
|
||||
}
|
||||
else
|
||||
|
||||
const valueArray: string[] = [];
|
||||
|
||||
if (!this.hasHeaderRow)
|
||||
{
|
||||
return ([this.headerValues[columnIndex], ...this.bodyValuesPreview[columnIndex]]);
|
||||
const typedValue = getTypedValue(this.headerValues[columnIndex]);
|
||||
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
||||
}
|
||||
|
||||
for (let value of this.bodyValuesPreview[columnIndex])
|
||||
{
|
||||
const typedValue = getTypedValue(value);
|
||||
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
||||
}
|
||||
|
||||
return (valueArray);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,11 @@ export default class QQueryColumns
|
||||
{
|
||||
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName)
|
||||
|
||||
if(!field)
|
||||
{
|
||||
console.warn(`Couldn't find field ${fieldName} in tableMetaData - so not adding a column for it`);
|
||||
}
|
||||
|
||||
let column: Column;
|
||||
if(tableForField.name == table.name)
|
||||
{
|
||||
|
@ -72,6 +72,7 @@ import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget"
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
@ -114,9 +115,14 @@ let formikSetTouched = ({}: any, touched: boolean): void =>
|
||||
|
||||
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||
|
||||
export interface SubFormPreSubmitCallbackResultType {maySubmit: boolean; values: {[name: string]: any}}
|
||||
export interface SubFormPreSubmitCallbackResultType
|
||||
{
|
||||
maySubmit: boolean;
|
||||
values: { [name: string]: any };
|
||||
}
|
||||
|
||||
type SubFormPreSubmitCallback = () => SubFormPreSubmitCallbackResultType;
|
||||
type SubFormPreSubmitCallbackWithName = {name: string, callback: SubFormPreSubmitCallback}
|
||||
type SubFormPreSubmitCallbackWithName = { name: string, callback: SubFormPreSubmitCallback }
|
||||
|
||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||
{
|
||||
@ -161,7 +167,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
|
||||
|
||||
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
||||
const [controlCallbacks, setControlCallbacks] = useState({} as {[name: string]: () => void});
|
||||
const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
|
||||
const [subFormPreSubmitCallbacks, setSubFormPreSubmitCallbacks] = useState([] as SubFormPreSubmitCallbackWithName[]);
|
||||
|
||||
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
|
||||
@ -237,7 +243,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const bulkLoadFileMappingFormRef = useRef();
|
||||
const bulkLoadValueMappingFormRef = useRef();
|
||||
const bulkLoadProfileFormRef = useRef();
|
||||
const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[])
|
||||
const [bulkLoadValueMappingFormFields, setBulkLoadValueMappingFormFields] = useState([] as any[]);
|
||||
|
||||
const doesStepHaveComponent = (step: QFrontendStepMetaData, type: QComponentType): boolean =>
|
||||
{
|
||||
@ -699,10 +705,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_FILE_MAPPING_FORM))
|
||||
{
|
||||
if(bulkLoadFileMappingFormRef?.current)
|
||||
if (bulkLoadFileMappingFormRef?.current)
|
||||
{
|
||||
// @ts-ignore ...
|
||||
addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit)
|
||||
addSubFormPreSubmitCallbacks("bulkLoadFileMappingForm", bulkLoadFileMappingFormRef?.current?.preSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -711,10 +717,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
|
||||
{
|
||||
if(bulkLoadValueMappingFormRef?.current)
|
||||
if (bulkLoadValueMappingFormRef?.current)
|
||||
{
|
||||
// @ts-ignore ...
|
||||
addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit)
|
||||
addSubFormPreSubmitCallbacks("bulkLoadValueMappingForm", bulkLoadValueMappingFormRef?.current?.preSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -723,10 +729,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_PROFILE_FORM))
|
||||
{
|
||||
if(bulkLoadProfileFormRef?.current)
|
||||
if (bulkLoadProfileFormRef?.current)
|
||||
{
|
||||
// @ts-ignore ...
|
||||
addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit)
|
||||
addSubFormPreSubmitCallbacks("bulkLoadProfileFormRef", bulkLoadProfileFormRef?.current?.preSubmit);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1032,9 +1038,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
<BulkLoadFileMappingForm
|
||||
processValues={processValues}
|
||||
tableMetaData={tableMetaData}
|
||||
processMetaData={processMetaData}
|
||||
metaData={qInstance}
|
||||
ref={bulkLoadFileMappingFormRef}
|
||||
setActiveStepLabel={setActiveStepLabel}
|
||||
frontendStep={activeStep}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1296,7 +1304,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// Help make this component's fields work with our formik form //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if(activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
|
||||
if (activeStep && doesStepHaveComponent(activeStep, QComponentType.BULK_LOAD_VALUE_MAPPING_FORM))
|
||||
{
|
||||
const fileValues = processValues.fileValues ?? [];
|
||||
const valueMapping = processValues.valueMapping ?? {};
|
||||
@ -1312,22 +1320,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
for (let i = 0; i < fileValues.length; i++)
|
||||
{
|
||||
const dynamicField = DynamicFormUtils.getDynamicField(qFieldMetaData);
|
||||
const wrappedField: any = {};
|
||||
const wrappedField: any = {};
|
||||
wrappedField[field.name] = dynamicField;
|
||||
DynamicFormUtils.addPossibleValueProps(wrappedField, [field], fieldTableName, null, null);
|
||||
|
||||
const initialValue = valueMapping[fileValues[i]];
|
||||
|
||||
if(dynamicField.possibleValueProps)
|
||||
if (dynamicField.possibleValueProps)
|
||||
{
|
||||
dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue]
|
||||
dynamicField.possibleValueProps.initialDisplayValue = mappedValueLabels[initialValue];
|
||||
}
|
||||
|
||||
addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null)
|
||||
addField(`${fieldFullName}.value.${i}`, dynamicField, initialValue, null);
|
||||
fieldsForComponent.push(dynamicField);
|
||||
}
|
||||
|
||||
setBulkLoadValueMappingFormFields(fieldsForComponent)
|
||||
setBulkLoadValueMappingFormFields(fieldsForComponent);
|
||||
}
|
||||
|
||||
if (Object.keys(dynamicFormFields).length > 0)
|
||||
@ -1520,15 +1528,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
***************************************************************************/
|
||||
function addSubFormPreSubmitCallbacks(name: string, callback: SubFormPreSubmitCallback)
|
||||
{
|
||||
if(subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
|
||||
if (subFormPreSubmitCallbacks.findIndex(c => c.name == name) == -1)
|
||||
{
|
||||
const newCallbacks: SubFormPreSubmitCallbackWithName[] = []
|
||||
for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
|
||||
const newCallbacks: SubFormPreSubmitCallbackWithName[] = [];
|
||||
for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
|
||||
{
|
||||
newCallbacks[i] = subFormPreSubmitCallbacks[i];
|
||||
}
|
||||
newCallbacks.push({name, callback})
|
||||
setSubFormPreSubmitCallbacks(newCallbacks)
|
||||
newCallbacks.push({name, callback});
|
||||
setSubFormPreSubmitCallbacks(newCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1618,7 +1626,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setRenderedWidgets({});
|
||||
setSubFormPreSubmitCallbacks([]);
|
||||
setQJobRunning(null);
|
||||
setBackStepName(qJobComplete.backStep)
|
||||
setBackStepName(qJobComplete.backStep);
|
||||
|
||||
if (formikSetFieldValueFunction)
|
||||
{
|
||||
@ -1813,8 +1821,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setProcessMetaData(processMetaData);
|
||||
setSteps(processMetaData.frontendSteps);
|
||||
|
||||
recordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
|
||||
recordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
|
||||
doRecordAnalytics({location: window.location, title: "Process: " + processMetaData?.label});
|
||||
doRecordAnalytics({category: "processEvents", action: "startProcess", label: processMetaData?.label});
|
||||
|
||||
if (processMetaData.tableName && !tableMetaData)
|
||||
{
|
||||
@ -1836,6 +1844,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)
|
||||
@ -1878,7 +1900,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
setTimeout(async () =>
|
||||
{
|
||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
|
||||
const processResponse = await qController.processStep(
|
||||
processName,
|
||||
@ -1898,7 +1920,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
setTimeout(async () =>
|
||||
{
|
||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
doRecordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
|
||||
const processResponse = await Client.getInstance().processStep(
|
||||
processName,
|
||||
@ -1922,20 +1944,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
///////////////////////////////////////////////////////////////
|
||||
// run any sub-form pre-submit callbacks that are registered //
|
||||
///////////////////////////////////////////////////////////////
|
||||
for(let i = 0; i < subFormPreSubmitCallbacks.length; i++)
|
||||
for (let i = 0; i < subFormPreSubmitCallbacks.length; i++)
|
||||
{
|
||||
const {maySubmit, values: moreValues} = subFormPreSubmitCallbacks[i].callback();
|
||||
if(!maySubmit)
|
||||
if (!maySubmit)
|
||||
{
|
||||
console.log(`May not submit form, per callback: ${subFormPreSubmitCallbacks[i].name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if(moreValues)
|
||||
if (moreValues)
|
||||
{
|
||||
for (let key in moreValues)
|
||||
{
|
||||
values[key] = moreValues[key]
|
||||
values[key] = moreValues[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2010,7 +2032,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
setLoadingRecords(true);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -2039,6 +2061,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function doRecordAnalytics(model: AnalyticsModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
recordAnalytics(model);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error recording analytics: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
const formStyles: any = {};
|
||||
if (isWidget)
|
||||
{
|
||||
@ -2220,7 +2257,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
if (isModal)
|
||||
{
|
||||
return (
|
||||
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
|
||||
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}} id="modalProcessScrollContainer">
|
||||
{body}
|
||||
</Box>
|
||||
);
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
import {ApiVersion} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
|
||||
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
@ -69,6 +70,7 @@ import RecordQueryView from "qqq/models/query/RecordQueryView";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
import ColumnStats from "qqq/pages/records/query/ColumnStats";
|
||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||
import {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
|
||||
@ -87,22 +89,25 @@ export type QueryScreenUsage = "queryScreen" | "reportSetup"
|
||||
|
||||
interface Props
|
||||
{
|
||||
table?: QTableMetaData;
|
||||
launchProcess?: QProcessMetaData;
|
||||
usage?: QueryScreenUsage;
|
||||
isModal?: boolean;
|
||||
isPreview?: boolean;
|
||||
initialQueryFilter?: QQueryFilter;
|
||||
initialColumns?: QQueryColumns;
|
||||
allowVariables?: boolean;
|
||||
table?: QTableMetaData,
|
||||
apiVersion?: ApiVersion,
|
||||
launchProcess?: QProcessMetaData,
|
||||
usage?: QueryScreenUsage,
|
||||
isModal?: boolean,
|
||||
isPreview?: boolean,
|
||||
initialQueryFilter?: QQueryFilter,
|
||||
initialColumns?: QQueryColumns,
|
||||
allowVariables?: boolean,
|
||||
omitExposedJoins?: string[]
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// define possible values for our pageState variable //
|
||||
///////////////////////////////////////////////////////
|
||||
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready";
|
||||
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready" | "error";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
const qControllerV1 = Client.getInstanceV1();
|
||||
|
||||
/*******************************************************************************
|
||||
** function to produce standard version of the screen while we're "loading"
|
||||
@ -126,7 +131,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
||||
**
|
||||
** Yuge component. The best. Lots of very smart people are saying so.
|
||||
*******************************************************************************/
|
||||
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||
const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns, omitExposedJoins}: Props, ref) =>
|
||||
{
|
||||
const tableName = table.name;
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -933,7 +938,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
}
|
||||
}
|
||||
|
||||
recordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
|
||||
doRecordAnalytics({category: "tableEvents", action: "query", label: tableMetaData.label});
|
||||
|
||||
console.log(`In updateTable for ${reason} ${JSON.stringify(queryFilter)}`);
|
||||
setLoading(true);
|
||||
@ -978,7 +983,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
}
|
||||
|
||||
let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables());
|
||||
qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
|
||||
// qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
|
||||
qControllerV1.count(tableName, apiVersion, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) =>
|
||||
{
|
||||
console.log(`Received count results for query ${thisQueryId}: ${count} ${distinctCount}`);
|
||||
countResults[thisQueryId] = [];
|
||||
@ -997,7 +1003,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
|
||||
setLastFetchedQFilterJSON(JSON.stringify(queryFilter));
|
||||
setLastFetchedVariant(tableVariant);
|
||||
qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
|
||||
// qController.query(tableName, filterForBackend, queryJoins, tableVariant).then((results) =>
|
||||
qControllerV1.query(tableName, apiVersion, filterForBackend, queryJoins, tableVariant).then((results) =>
|
||||
{
|
||||
console.log(`Received results for query ${thisQueryId}`);
|
||||
queryResults[thisQueryId] = results;
|
||||
@ -1103,7 +1110,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
////////////////////////////////
|
||||
// make the rows for the grid //
|
||||
////////////////////////////////
|
||||
const rows = DataGridUtils.makeRows(results, tableMetaData);
|
||||
const rows = DataGridUtils.makeRows(results, tableMetaData, tableVariant);
|
||||
setRows(rows);
|
||||
|
||||
setLoading(false);
|
||||
@ -1140,6 +1147,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
const handlePageNumberChange = (page: number) =>
|
||||
{
|
||||
setPageNumber(page);
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
@ -1148,6 +1156,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
const handleRowsPerPageChange = (size: number) =>
|
||||
{
|
||||
setRowsPerPage(size);
|
||||
setLoading(true);
|
||||
|
||||
view.rowsPerPage = size;
|
||||
doSetView(view);
|
||||
@ -1612,6 +1621,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);
|
||||
@ -1655,8 +1680,9 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
{
|
||||
if (savedViewRecord == null)
|
||||
{
|
||||
console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView instead.");
|
||||
console.log("doSetCurrentView called with a null view record - calling doClearCurrentSavedView, and activating tableDefaultView instead.");
|
||||
doClearCurrentSavedView();
|
||||
activateView(buildTableDefaultView(tableMetaData));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1707,7 +1733,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
{
|
||||
if (selectedSavedViewId != null)
|
||||
{
|
||||
recordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
|
||||
doRecordAnalytics({category: "tableEvents", action: "activateSavedView", label: tableMetaData.label});
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// fetch, then activate the selected filter //
|
||||
@ -1724,7 +1750,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
/////////////////////////////////
|
||||
// this is 'new view' - right? //
|
||||
/////////////////////////////////
|
||||
recordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
|
||||
doRecordAnalytics({category: "tableEvents", action: "activateNewView", label: tableMetaData.label});
|
||||
|
||||
//////////////////////////////
|
||||
// wipe away the saved view //
|
||||
@ -1752,7 +1778,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError;
|
||||
console.error("Could not retrieve saved filter: " + jobError.userFacingError);
|
||||
console.error("Could not retrieve saved view: " + jobError.userFacingError);
|
||||
setAlertContent("There was an error loading the selected view.");
|
||||
}
|
||||
else
|
||||
@ -2418,23 +2444,33 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
const metaData = await qController.loadMetaData();
|
||||
setMetaData(metaData);
|
||||
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTableMetaData(tableMetaData);
|
||||
setTableLabel(tableMetaData.label);
|
||||
try
|
||||
{
|
||||
// const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
const tableMetaData = await qControllerV1.loadTableMetaData(tableName, apiVersion);
|
||||
setTableMetaData(tableMetaData);
|
||||
setTableLabel(tableMetaData.label);
|
||||
|
||||
recordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
|
||||
doRecordAnalytics({location: window.location, title: "Query: " + tableMetaData.label});
|
||||
|
||||
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
|
||||
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
|
||||
setTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
|
||||
setAllTableProcesses(ProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
|
||||
// but also used when user selects new-view from the view menu //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const newDefaultView = buildTableDefaultView(tableMetaData);
|
||||
setTableDefaultView(newDefaultView);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now that we know the table - build a default view - initially, only used by SavedViews component, for showing if there's anything to be saved. //
|
||||
// but also used when user selects new-view from the view menu //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const newDefaultView = buildTableDefaultView(tableMetaData);
|
||||
setTableDefaultView(newDefaultView);
|
||||
|
||||
setPageState("loadedMetaData");
|
||||
setPageState("loadedMetaData");
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
setPageState("error");
|
||||
//@ts-ignore e.message
|
||||
setAlertContent("Error loading table: " + e?.message ?? "Details not available.");
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@ -2702,6 +2738,16 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// render an error screen (alert) if needed //
|
||||
//////////////////////////////////////////////
|
||||
if (pageState == "error")
|
||||
{
|
||||
console.log(`page state is ${pageState}... rendering an alert...`);
|
||||
const errorBody = <Box py={3}><Alert severity="error">{alertContent}</Alert></Box>;
|
||||
return isModal ? errorBody : <BaseLayout>{errorBody}</BaseLayout>;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// render a loading screen if the page state isn't ready //
|
||||
///////////////////////////////////////////////////////////
|
||||
@ -2789,6 +2835,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
idPrefix="columns"
|
||||
tableMetaData={tableMetaData}
|
||||
showTableHeaderEvenIfNoExposedJoins={true}
|
||||
omitExposedJoins={omitExposedJoins}
|
||||
placeholder="Search Fields"
|
||||
buttonProps={{sx: columnMenuButtonStyles}}
|
||||
buttonChildren={<><Icon sx={{mr: "0.5rem"}}>view_week_outline</Icon> Columns ({view.queryColumns.getVisibleColumnCount()}) <Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon></>}
|
||||
@ -2799,6 +2846,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
</Box>);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function doRecordAnalytics(model: AnalyticsModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
recordAnalytics(model);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(`Error recording analytics: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// these numbers help set the height of the grid (so page won't scroll) based on space above & below it //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -2914,6 +2977,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
setMode={doSetMode}
|
||||
savedViewsComponent={savedViewsComponent}
|
||||
columnMenuComponent={buildColumnMenu()}
|
||||
omitExposedJoins={omitExposedJoins}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -2939,7 +3003,8 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
metaData: metaData,
|
||||
queryFilter: queryFilter,
|
||||
updateFilter: doSetQueryFilter,
|
||||
allowVariables: allowVariables
|
||||
allowVariables: allowVariables,
|
||||
omitExposedJoins: omitExposedJoins,
|
||||
}
|
||||
}}
|
||||
localeText={{
|
||||
@ -3036,6 +3101,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
||||
|
||||
RecordQuery.defaultProps = {
|
||||
table: null,
|
||||
apiVersion: null,
|
||||
usage: "queryScreen",
|
||||
launchProcess: null,
|
||||
isModal: false,
|
||||
|
@ -191,7 +191,7 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
||||
<Card sx={{mb: 3}}>
|
||||
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mx={3} mb={3} mt={0}>
|
||||
{scriptId ?
|
||||
<ScriptViewer
|
||||
scriptId={scriptId}
|
||||
|
@ -92,9 +92,9 @@ const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps})
|
||||
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps}, tableVariant?: QTableVariant)
|
||||
{
|
||||
return <Box key={key} display="flex" flexDirection="column" py={1} pr={2}>
|
||||
return <Grid container lg={12} key={key} display="flex" py={1} pr={2}>
|
||||
{
|
||||
fieldNames.map((fieldName: string) =>
|
||||
{
|
||||
@ -103,6 +103,7 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
||||
if (field != null)
|
||||
{
|
||||
let label = field.label;
|
||||
let gridColumns = (field.gridColumns && field.gridColumns > 0) ? field.gridColumns : 12;
|
||||
|
||||
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
||||
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
|
||||
@ -111,22 +112,22 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
||||
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default", ...(styleOverrides?.label ?? {})}}>{label}:</Typography>;
|
||||
|
||||
return (
|
||||
<Box key={fieldName} flexDirection="row" pr={2}>
|
||||
<Grid item key={fieldName} lg={gridColumns} flexDirection="column" pr={2}>
|
||||
<>
|
||||
{
|
||||
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
|
||||
}
|
||||
<div style={{display: "inline-block", width: 0}}> </div>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)" sx={{...(styleOverrides?.value ?? {})}}>
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName, tableVariant)}
|
||||
</Typography>
|
||||
</>
|
||||
</Box>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
</Box>;
|
||||
</Grid>;
|
||||
}
|
||||
|
||||
|
||||
@ -439,6 +440,34 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function getGenericProcesses(metaData: QInstance)
|
||||
{
|
||||
const genericProcesses: QProcessMetaData[] = [];
|
||||
const materialDashboardInstanceMetaData = metaData?.supplementalInstanceMetaData?.get("materialDashboard");
|
||||
if (materialDashboardInstanceMetaData)
|
||||
{
|
||||
const processNamesToAddToAllQueryAndViewScreens = materialDashboardInstanceMetaData.processNamesToAddToAllQueryAndViewScreens;
|
||||
if (processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
for (let processName of processNamesToAddToAllQueryAndViewScreens)
|
||||
{
|
||||
genericProcesses.push(metaData?.processes?.get(processName));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////
|
||||
// deprecated //
|
||||
////////////////
|
||||
genericProcesses.push(metaData?.processes.get("runRecordScript"));
|
||||
}
|
||||
return genericProcesses;
|
||||
}
|
||||
|
||||
if (!asyncLoadInited)
|
||||
{
|
||||
setAsyncLoadInited(true);
|
||||
@ -471,11 +500,16 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
// load processes that the routing needs to respect //
|
||||
//////////////////////////////////////////////////////
|
||||
const allTableProcesses = ProcessUtils.getProcessesForTable(metaData, tableName, true); // these include hidden ones (e.g., to find the bulks)
|
||||
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||
if (runRecordScriptProcess)
|
||||
const genericProcesses = getGenericProcesses(metaData);
|
||||
|
||||
for (let genericProcess of genericProcesses)
|
||||
{
|
||||
allTableProcesses.unshift(runRecordScriptProcess);
|
||||
if (genericProcess)
|
||||
{
|
||||
allTableProcesses.unshift(genericProcess);
|
||||
}
|
||||
}
|
||||
|
||||
setAllTableProcesses(allTableProcesses);
|
||||
|
||||
if (launchingProcess)
|
||||
@ -597,7 +631,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
// for a section with field names, render the field values. //
|
||||
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record);
|
||||
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record, undefined, undefined, tableVariant);
|
||||
|
||||
if (section.tier === "T1")
|
||||
{
|
||||
@ -725,7 +759,6 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
|
||||
let hasEditOrDelete = (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission) || (table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission);
|
||||
|
||||
const runRecordScriptProcess = metaData?.processes.get("runRecordScript");
|
||||
|
||||
const renderActionsMenu = (
|
||||
<Menu
|
||||
@ -784,11 +817,14 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
))}
|
||||
{(tableProcesses?.length > 0 || hasEditOrDelete) && <Divider />}
|
||||
{
|
||||
runRecordScriptProcess &&
|
||||
<MenuItem key={runRecordScriptProcess.name} onClick={() => processClicked(runRecordScriptProcess)}>
|
||||
<ListItemIcon><Icon>{runRecordScriptProcess.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||
{runRecordScriptProcess.label}
|
||||
</MenuItem>
|
||||
getGenericProcesses(metaData).map((process) =>
|
||||
(
|
||||
process &&
|
||||
<MenuItem key={process.name} onClick={() => processClicked(process)}>
|
||||
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
|
||||
{process.label}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
<MenuItem onClick={() => navigate("dev")}>
|
||||
<ListItemIcon><Icon>code</Icon></ListItemIcon>
|
||||
@ -968,7 +1004,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
{
|
||||
notFoundMessage
|
||||
?
|
||||
<Alert color="error" sx={{mb: 3}}>{notFoundMessage}</Alert>
|
||||
<Alert color="error" sx={{mb: 3}} icon={<Icon>warning</Icon>}>{notFoundMessage}</Alert>
|
||||
:
|
||||
<Box pb={3}>
|
||||
{
|
||||
@ -1045,16 +1081,19 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
||||
</React.Fragment>
|
||||
)) : null}
|
||||
</Grid>
|
||||
<Box component="form" p={3}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
|
||||
}
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && <QEditButton />
|
||||
}
|
||||
</Grid>
|
||||
</Box>
|
||||
{
|
||||
tableMetaData && record && ((table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission) || (table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission)) &&
|
||||
<Box component="div" p={3} className={"stickyBottomButtonBar"}>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_DELETE) && table.deletePermission && <QDeleteButton onClickHandler={handleClickDeleteButton} />
|
||||
}
|
||||
{
|
||||
table.capabilities.has(Capability.TABLE_UPDATE) && table.editPermission && <QEditButton />
|
||||
}
|
||||
</Grid>
|
||||
</Box>
|
||||
}
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -303,10 +303,15 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
.MuiTablePagination-root .MuiSvgIcon-root
|
||||
{
|
||||
display: inline;
|
||||
color: gray;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
right: 0.125rem;
|
||||
}
|
||||
|
||||
.MuiTablePagination-root .Mui-disabled .MuiSvgIcon-root
|
||||
{
|
||||
color: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.devDocumentation ul > li
|
||||
{
|
||||
margin-left: 30px;
|
||||
@ -748,35 +753,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;
|
||||
}
|
||||
@ -817,4 +841,27 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
max-width: 100% !important;
|
||||
flex-grow: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stickyBottomButtonBar
|
||||
{
|
||||
padding-bottom: 1rem !important;
|
||||
padding-right: 0 !important;
|
||||
margin-bottom: -4rem !important;
|
||||
margin-top: -1.5rem !important;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, transparent 0, #f0f2f5 4px);
|
||||
z-index: 10; /* have needed a little here, e.g. to get above MuiDataGrid-overlay and ACE */
|
||||
}
|
||||
|
||||
.modalBottomButtonBar
|
||||
{
|
||||
padding-bottom: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.stickyBottomButtonBar>.MuiGrid-container
|
||||
{
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {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";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
||||
@ -70,7 +71,7 @@ export default class DataGridUtils
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, allowEmptyId = false): GridRowsProp[] =>
|
||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, tableVariant?: QTableVariant, allowEmptyId = false): GridRowsProp[] =>
|
||||
{
|
||||
const fields = [...tableMetaData.fields.values()];
|
||||
const rows = [] as any[];
|
||||
@ -82,7 +83,7 @@ export default class DataGridUtils
|
||||
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query", undefined, tableVariant);
|
||||
});
|
||||
|
||||
if (tableMetaData.exposedJoins)
|
||||
@ -97,7 +98,7 @@ export default class DataGridUtils
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
let fieldName = join.joinTable.name + "." + field.name;
|
||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName);
|
||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName, tableVariant);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export default class GoogleAnalyticsUtils
|
||||
console.log("Error reading session values from localStorage: " + e);
|
||||
}
|
||||
|
||||
if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||
if (this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||
{
|
||||
this.active = true;
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
import {QControllerV1} from "@kingsrook/qqq-frontend-core/lib/controllers/QControllerV1";
|
||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||
|
||||
/*******************************************************************************
|
||||
@ -29,6 +30,7 @@ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException
|
||||
class Client
|
||||
{
|
||||
private static qController: QController;
|
||||
private static qControllerV1: QControllerV1;
|
||||
private static unauthorizedCallback: () => void;
|
||||
|
||||
private static handleException(exception: QException)
|
||||
@ -54,6 +56,22 @@ class Client
|
||||
return this.qController;
|
||||
}
|
||||
|
||||
public static getInstanceV1(path: string = "/qqq/v1")
|
||||
{
|
||||
if (this.qControllerV1 == null)
|
||||
{
|
||||
this.qControllerV1 = new QControllerV1(path, this.handleException);
|
||||
}
|
||||
|
||||
return this.qControllerV1;
|
||||
}
|
||||
|
||||
public static setGotAuthenticationInAllControllers()
|
||||
{
|
||||
Client.getInstance().setGotAuthentication();
|
||||
Client.getInstanceV1().setGotAuthentication();
|
||||
}
|
||||
|
||||
static setUnauthorizedCallback(unauthorizedCallback: () => void)
|
||||
{
|
||||
Client.unauthorizedCallback = unauthorizedCallback;
|
||||
|
@ -108,6 +108,12 @@ class FilterUtils
|
||||
const criteria = queryFilter.criteria[i];
|
||||
let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
|
||||
|
||||
if(!field)
|
||||
{
|
||||
console.warn(`Field ${criteria.fieldName} not found in tableMetaData - unable to clean up values for it..`);
|
||||
return;
|
||||
}
|
||||
|
||||
let values = criteria.values;
|
||||
let hasFilterVariable = false;
|
||||
|
||||
@ -133,7 +139,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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,21 +407,21 @@ class FilterUtils
|
||||
{
|
||||
const expression = new ThisOrLastPeriodExpression(value);
|
||||
let startOfPrefix = "";
|
||||
if (fieldMetaData.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
|
||||
if (fieldMetaData?.type == QFieldType.DATE_TIME || expression.timeUnit != "DAYS")
|
||||
{
|
||||
startOfPrefix = "start of ";
|
||||
}
|
||||
labels.push(`${startOfPrefix}${expression.toString()}`);
|
||||
}
|
||||
else if (fieldMetaData.type == QFieldType.BOOLEAN)
|
||||
else if (fieldMetaData?.type == QFieldType.BOOLEAN)
|
||||
{
|
||||
labels.push(value == true ? "yes" : "no");
|
||||
}
|
||||
else if (fieldMetaData.type == QFieldType.DATE_TIME)
|
||||
else if (fieldMetaData?.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
labels.push(ValueUtils.formatDateTime(value));
|
||||
}
|
||||
else if (fieldMetaData.type == QFieldType.DATE)
|
||||
else if (fieldMetaData?.type == QFieldType.DATE)
|
||||
{
|
||||
labels.push(ValueUtils.formatDate(value));
|
||||
}
|
||||
|
361
src/qqq/utils/qqq/QFMDBridge.tsx
Normal file
361
src/qqq/utils/qqq/QFMDBridge.tsx
Normal file
@ -0,0 +1,361 @@
|
||||
/*
|
||||
* 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 {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Alert} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import {ThemeProvider} from "@mui/material/styles";
|
||||
import {Formik} from "formik";
|
||||
import QContext from "QContext";
|
||||
import QDynamicForm from "qqq/components/forms/DynamicForm";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import MDButton from "qqq/components/legacy/MDButton";
|
||||
import theme from "qqq/components/legacy/Theme";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import {MaterialUIControllerProvider} from "qqq/context";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {ReactElement, ReactNode, useContext, useEffect, useState} from "react";
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
import * as Yup from "yup";
|
||||
|
||||
|
||||
// todo - deploy this interface somehow out of this file
|
||||
export interface QFMDBridge
|
||||
{
|
||||
qController?: QController;
|
||||
makeAlert: (text: string, color: string) => JSX.Element;
|
||||
makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }) => JSX.Element;
|
||||
makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void) => JSX.Element;
|
||||
makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void) => JSX.Element;
|
||||
makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean) => JSX.Element;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Component to generate a form for the QFMD Bridge
|
||||
***************************************************************************/
|
||||
interface QFMDBridgeFormProps
|
||||
{
|
||||
fields: QFieldMetaData[],
|
||||
record: QRecord,
|
||||
handleChange: (fieldName: string, newValue: any) => void,
|
||||
handleSubmit: (values: any) => void
|
||||
}
|
||||
|
||||
QFMDBridgeForm.defaultProps = {};
|
||||
|
||||
function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridgeFormProps): JSX.Element
|
||||
{
|
||||
const initialValues: any = {};
|
||||
for (let field of fields)
|
||||
{
|
||||
initialValues[field.name] = record.values.get(field.name);
|
||||
if(initialValues[field.name] === undefined && field.defaultValue !== undefined)
|
||||
{
|
||||
initialValues[field.name] = field.defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
const [lastValues, setLastValues] = useState(initialValues);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// store reference to record display values in a state var - see usage below //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
const [recordDisplayValues, setRecordDisplayValues] = useState(record?.displayValues ?? new Map<string, string>())
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
const qController = Client.getInstance();
|
||||
|
||||
for (let field of fields)
|
||||
{
|
||||
const value = record.values.get(field.name);
|
||||
if (field.possibleValueSourceName && value)
|
||||
{
|
||||
const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], [], record.values, "form");
|
||||
if (possibleValues && possibleValues.length > 0)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// originally, we put this in record.displayValues, but, sometimes that would then be empty at the usage point below... //
|
||||
// this works, so, we'll go with it //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
recordDisplayValues.set(field.name, possibleValues[0].label)
|
||||
setRecordDisplayValues(recordDisplayValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLoaded(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!loaded)
|
||||
{
|
||||
return (<Box py={"1rem"}>Loading...</Box>);
|
||||
}
|
||||
|
||||
const {
|
||||
dynamicFormFields,
|
||||
formValidations,
|
||||
} = DynamicFormUtils.getFormData(fields);
|
||||
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, recordDisplayValues);
|
||||
|
||||
const otherValuesMap = new Map<string, any>();
|
||||
record.values.forEach((value, key) => otherValuesMap.set(key, value));
|
||||
|
||||
for (let fieldName in dynamicFormFields)
|
||||
{
|
||||
const dynamicFormField = dynamicFormFields[fieldName];
|
||||
if (dynamicFormField.possibleValueProps)
|
||||
{
|
||||
dynamicFormField.possibleValueProps.otherValues = otherValuesMap;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// re-introduce these two context providers, in case the child calls this //
|
||||
// method under a different root... maybe this should be optional per a param? //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
return (<MaterialUIControllerProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Formik initialValues={initialValues} validationSchema={Yup.object().shape(formValidations)} onSubmit={handleSubmit}>
|
||||
{({values, errors, touched}) =>
|
||||
{
|
||||
const formData: any = {};
|
||||
formData.values = values;
|
||||
formData.touched = touched;
|
||||
formData.errors = errors;
|
||||
formData.formFields = dynamicFormFields;
|
||||
|
||||
try
|
||||
{
|
||||
let anyDiffs = false;
|
||||
for (let fieldName in values)
|
||||
{
|
||||
const value = values[fieldName];
|
||||
if (lastValues[fieldName] != value)
|
||||
{
|
||||
handleChange(fieldName, value);
|
||||
lastValues[fieldName] = value;
|
||||
anyDiffs = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyDiffs)
|
||||
{
|
||||
setLastValues(lastValues);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return (<QDynamicForm formData={formData} record={record} />);
|
||||
}}
|
||||
</Formik>
|
||||
</ThemeProvider>
|
||||
</MaterialUIControllerProvider>);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Component to render a widget for the QFMD Bridge
|
||||
***************************************************************************/
|
||||
interface QFMDBridgeWidgetProps
|
||||
{
|
||||
widgetName?: string,
|
||||
tableName?: string,
|
||||
record?: QRecord,
|
||||
entityPrimaryKey?: string,
|
||||
actionCallback?: (data: any, eventValues?: { [p: string]: any }) => boolean
|
||||
}
|
||||
|
||||
QFMDBridgeWidget.defaultProps = {};
|
||||
|
||||
function QFMDBridgeWidget({widgetName, tableName, record, entityPrimaryKey, actionCallback}: QFMDBridgeWidgetProps): JSX.Element
|
||||
{
|
||||
const qContext = useContext(QContext);
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const [widgetMetaData, setWidgetMetaData] = useState(null as QWidgetMetaData);
|
||||
const [widgetData, setWidgetData] = useState(null as any);
|
||||
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
const qController = Client.getInstance();
|
||||
const qInstance = await qController.loadMetaData();
|
||||
|
||||
const queryStringParts: string[] = [];
|
||||
for (let key of record?.values?.keys())
|
||||
{
|
||||
queryStringParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(record.values.get(key))}`);
|
||||
}
|
||||
|
||||
setWidgetMetaData(qInstance.widgets.get(widgetName));
|
||||
setWidgetData(await qController.widget(widgetName, queryStringParts.join("&")));
|
||||
|
||||
setReady(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (!ready)
|
||||
{
|
||||
return (<Box py={"1rem"}>Loading...</Box>);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// internally in some widgets, useNavigate happens... so we must re-introduce the browser-router context //
|
||||
// plus the contexts too, as indicated. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (<BrowserRouter>
|
||||
<MaterialUIControllerProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<QContext.Provider value={{
|
||||
...qContext,
|
||||
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
|
||||
}}>
|
||||
<div className={`bridgedWidget ${widgetMetaData.type}`}>
|
||||
<DashboardWidgets tableName={tableName} widgetMetaDataList={[widgetMetaData]} initialWidgetDataList={[widgetData]} record={record} entityPrimaryKey={entityPrimaryKey} omitWrappingGridContainer={true} actionCallback={actionCallback} />
|
||||
</div>
|
||||
</QContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MaterialUIControllerProvider>
|
||||
</BrowserRouter>);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Component to render a modal for the QFMD Bridge
|
||||
***************************************************************************/
|
||||
interface QFMDBridgeModalProps
|
||||
{
|
||||
children: ReactNode;
|
||||
onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||
}
|
||||
|
||||
QFMDBridgeModal.defaultProps = {};
|
||||
|
||||
function QFMDBridgeModal({children, onClose}: QFMDBridgeModalProps): JSX.Element
|
||||
{
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
function closeModalProcess(event: {}, reason: "backdropClick" | "escapeKeyDown")
|
||||
{
|
||||
if (onClose)
|
||||
{
|
||||
onClose(setIsOpen, event, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onClose={(event, reason) => closeModalProcess(event, reason)}>
|
||||
<Box className="bridgeModal" height="calc(100vh)">
|
||||
{children}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Component to render an alert for the QFMD Bridge
|
||||
***************************************************************************/
|
||||
interface QFMDBridgeAlertProps
|
||||
{
|
||||
color: string,
|
||||
children: ReactNode,
|
||||
mayManuallyClose?: boolean
|
||||
}
|
||||
|
||||
QFMDBridgeAlert.defaultProps = {};
|
||||
|
||||
function QFMDBridgeAlert({color, children, mayManuallyClose}: QFMDBridgeAlertProps): JSX.Element
|
||||
{
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
function onClose()
|
||||
{
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if (isOpen)
|
||||
{
|
||||
//@ts-ignore color
|
||||
return (<Alert color={color} onClose={mayManuallyClose ? onClose : null}>{children}</Alert>);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<React.Fragment />);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** define the default qfmd bridge object
|
||||
***************************************************************************/
|
||||
export const qfmdBridge =
|
||||
{
|
||||
qController: Client.getInstance(),
|
||||
|
||||
makeButton: (label: string, onClick: () => void, extra?: { [key: string]: any }): JSX.Element =>
|
||||
{
|
||||
return (<MDButton {...extra} onClick={onClick} fullWidth>{label}</MDButton>);
|
||||
},
|
||||
|
||||
makeAlert: (text: string, color: string, mayManuallyClose?: boolean): JSX.Element =>
|
||||
{
|
||||
return (<QFMDBridgeAlert color={color} mayManuallyClose={mayManuallyClose}>{text}</QFMDBridgeAlert>);
|
||||
},
|
||||
|
||||
makeModal: (children: ReactElement, onClose?: (setIsOpen: (isOpen: boolean) => void, event: {}, reason: "backdropClick" | "escapeKeyDown") => void): JSX.Element =>
|
||||
{
|
||||
return (<QFMDBridgeModal onClose={onClose}>{children}</QFMDBridgeModal>);
|
||||
},
|
||||
|
||||
makeWidget: (widgetName: string, tableName?: string, entityPrimaryKey?: string, record?: QRecord, actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean): JSX.Element =>
|
||||
{
|
||||
return (<QFMDBridgeWidget widgetName={widgetName} tableName={tableName} record={record} entityPrimaryKey={entityPrimaryKey} actionCallback={actionCallback} />);
|
||||
},
|
||||
|
||||
makeForm: (fields: QFieldMetaData[], record: QRecord, handleChange: (fieldName: string, newValue: any) => void, handleSubmit: (values: any) => void): JSX.Element =>
|
||||
{
|
||||
return (<QFMDBridgeForm fields={fields} record={record} handleChange={handleChange} handleSubmit={handleSubmit} />);
|
||||
}
|
||||
};
|
||||
|
@ -37,7 +37,7 @@ export class SavedBulkLoadProfileUtils
|
||||
|
||||
for (let bulkLoadField of orderedFieldArray)
|
||||
{
|
||||
const fieldName = bulkLoadField.field.name;
|
||||
const fieldName = bulkLoadField.getQualifiedName()
|
||||
const compareField = compareFieldsMap[fieldName];
|
||||
const baseField = baseFieldsMap[fieldName];
|
||||
if(!compareField)
|
||||
@ -55,12 +55,13 @@ export class SavedBulkLoadProfileUtils
|
||||
if (compareField.valueType == "column")
|
||||
{
|
||||
const column = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a default value (${baseField.defaultValue}) to using a file column (${column})`);
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a default value (${baseField.defaultValue}) to using a file column ${column ? `(${column})` : ""}`);
|
||||
}
|
||||
else if (compareField.valueType == "defaultValue")
|
||||
{
|
||||
const column = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a file column (${column}) to using a default value (${compareField.defaultValue})`);
|
||||
const value = compareField.defaultValue;
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a file column (${column}) to using a default value ${value === undefined ? "" : `(${value})`}`);
|
||||
}
|
||||
}
|
||||
else if (baseField.valueType == compareField.valueType && baseField.valueType == "defaultValue")
|
||||
@ -70,7 +71,8 @@ export class SavedBulkLoadProfileUtils
|
||||
//////////////////////////////////////////////////
|
||||
if (baseField.defaultValue != compareField.defaultValue)
|
||||
{
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} default value from (${baseField.defaultValue}) to (${compareField.defaultValue})`);
|
||||
const value = compareField.defaultValue;
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} default value from (${baseField.defaultValue}) to ${value === undefined ? "" : `(${value})`}`);
|
||||
}
|
||||
}
|
||||
else if (baseField.valueType == compareField.valueType && baseField.valueType == "column")
|
||||
@ -78,25 +80,29 @@ export class SavedBulkLoadProfileUtils
|
||||
///////////////////////////////////////////
|
||||
// if we changed the column, report that //
|
||||
///////////////////////////////////////////
|
||||
let isDiff = false;
|
||||
if (fileDescription.hasHeaderRow)
|
||||
{
|
||||
if (baseField.headerName != compareField.headerName)
|
||||
{
|
||||
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from (${baseColumn}) to (${compareColumn})`);
|
||||
isDiff = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (baseField.columnIndex != compareField.columnIndex)
|
||||
{
|
||||
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from (${baseColumn}) to (${compareColumn})`);
|
||||
isDiff = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(isDiff)
|
||||
{
|
||||
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from ${baseColumn ? `(${baseColumn})` : "--"} to ${compareColumn ? `(${compareColumn})` : "--"}`);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the do-value-mapping field changed, report that (note, only if was and still is column-type) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -120,7 +126,7 @@ export class SavedBulkLoadProfileUtils
|
||||
|
||||
for (let bulkLoadField of orderedFieldArray)
|
||||
{
|
||||
const fieldName = bulkLoadField.field.name;
|
||||
const fieldName = bulkLoadField.getQualifiedName()
|
||||
const compareField = compareFieldsMap[fieldName];
|
||||
if(!compareField)
|
||||
{
|
||||
@ -292,7 +298,7 @@ export class SavedBulkLoadProfileUtils
|
||||
{
|
||||
try
|
||||
{
|
||||
const fieldName = bulkLoadField.field.name;
|
||||
const fieldName = bulkLoadField.getQualifiedName() // todo - does this (and the others calls to this) need suffix?
|
||||
|
||||
const valueMappingDiff = this.diffFieldValueMappings(bulkLoadField, baseMapping.valueMappings[fieldName] ?? {}, activeMapping.valueMappings[fieldName] ?? {});
|
||||
if(valueMappingDiff)
|
||||
|
@ -23,6 +23,7 @@ import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Ado
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import "datejs"; // https://github.com/datejs/Datejs
|
||||
import {Chip, ClickAwayListener, Icon} from "@mui/material";
|
||||
@ -76,14 +77,14 @@ class ValueUtils
|
||||
** When you have a field, and a record - call this method to get a string or
|
||||
** element back to display the field's value.
|
||||
*******************************************************************************/
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string): string | JSX.Element | JSX.Element[]
|
||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string, tableVariant?: QTableVariant): string | JSX.Element | JSX.Element[]
|
||||
{
|
||||
const fieldName = overrideFieldName ?? field.name;
|
||||
|
||||
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
|
||||
const rawValue = record.values ? record.values.get(fieldName) : undefined;
|
||||
|
||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
|
||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage, tableVariant, record, fieldName);
|
||||
}
|
||||
|
||||
|
||||
@ -91,14 +92,35 @@ class ValueUtils
|
||||
** When you have a field and a value (either just a raw value, or a raw and
|
||||
** display value), call this method to get a string Element to display.
|
||||
*******************************************************************************/
|
||||
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view"): string | JSX.Element | JSX.Element[]
|
||||
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view", tableVariant?: QTableVariant, record?: QRecord, fieldName?: string): string | JSX.Element | JSX.Element[]
|
||||
{
|
||||
if (field.hasAdornment(AdornmentType.LINK))
|
||||
{
|
||||
const adornment = field.getAdornment(AdornmentType.LINK);
|
||||
let href = rawValue;
|
||||
let href = String(rawValue);
|
||||
|
||||
let toRecordFromTable = adornment.getValue("toRecordFromTable");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the link adornment has a 'toRecordFromTableDynamic', then look for a display //
|
||||
// value named `fieldName`:toRecordFromTableDynamic for the table name. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if(adornment.getValue("toRecordFromTableDynamic"))
|
||||
{
|
||||
const toRecordFromTableDynamic = record?.displayValues?.get(fieldName + ":toRecordFromTableDynamic");
|
||||
if(toRecordFromTableDynamic)
|
||||
{
|
||||
toRecordFromTable = toRecordFromTableDynamic;
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if the table name isn't known, then return w/o the adornment. //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
}
|
||||
|
||||
const toRecordFromTable = adornment.getValue("toRecordFromTable");
|
||||
if (toRecordFromTable)
|
||||
{
|
||||
if (ValueUtils.getQInstance())
|
||||
@ -107,7 +129,7 @@ class ValueUtils
|
||||
if (!tablePath)
|
||||
{
|
||||
console.log("Couldn't find path for table: " + toRecordFromTable);
|
||||
return (displayValue ?? rawValue);
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
|
||||
if (!tablePath.endsWith("/"))
|
||||
@ -199,12 +221,44 @@ class ValueUtils
|
||||
|
||||
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||
{
|
||||
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
|
||||
let url = rawValue;
|
||||
if(tableVariant)
|
||||
{
|
||||
url += "?tableVariant=" + encodeURIComponent(JSON.stringify(tableVariant));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the field has the download adornment with a downloadUrlDynamic value, //
|
||||
// then get the url from a displayValue of `fieldName`:downloadUrlDynamic. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
if(field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||
{
|
||||
const adornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
|
||||
let downloadUrlDynamicAdornmentValue = adornment.getValue("downloadUrlDynamic");
|
||||
if(downloadUrlDynamicAdornmentValue)
|
||||
{
|
||||
const downloadUrlDynamicValue = record?.displayValues?.get(fieldName + ":downloadUrlDynamic");
|
||||
if (downloadUrlDynamicValue)
|
||||
{
|
||||
url = downloadUrlDynamicValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// if the url isn't available, then return w/o the adornment. //
|
||||
////////////////////////////////////////////////////////////////
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (<BlobComponent field={field} url={url} filename={displayValue} usage={usage} />);
|
||||
}
|
||||
|
||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** After we know there's no element to be returned (e.g., because no adornment),
|
||||
** this method does the string formatting.
|
||||
@ -213,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)
|
||||
|
117
src/qqq/utils/qqq/useDynamicComponents.tsx
Normal file
117
src/qqq/utils/qqq/useDynamicComponents.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 Box from "@mui/material/Box";
|
||||
import {qfmdBridge, QFMDBridge} from "qqq/utils/qqq/QFMDBridge";
|
||||
import React, {useState} from "react";
|
||||
|
||||
// todo - deploy from here!!
|
||||
interface DynamicComponentProps
|
||||
{
|
||||
qfmdBridge?: QFMDBridge;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** hook for working with Dynamically loaded components
|
||||
**
|
||||
*******************************************************************************/
|
||||
export default function useDynamicComponents()
|
||||
{
|
||||
const [dynamicComponents, setDynamicComponents] = useState<{ [name: string]: React.FC }>({});
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const loadComponent = async (name: string, url: string) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
////////////////////////////////////////////////////////
|
||||
// Dynamically load the bundle by adding a script tag //
|
||||
////////////////////////////////////////////////////////
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
////////////////////////////////////////////////
|
||||
// if the script can't be loaded log an error //
|
||||
////////////////////////////////////////////////
|
||||
console.error(`Error loading bundle from [${url}]`);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Assuming the bundle attaches itself to window.${name} (.${name} again...) //
|
||||
// (Note: if exported as UMD, you might need to access the default export) //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
let component = (window as any)[name]?.[name];
|
||||
if (!component)
|
||||
{
|
||||
console.error(`Component not found on window.${name}`);
|
||||
component = () => <Box>Error loading {name}</Box>;
|
||||
}
|
||||
|
||||
const newDCs = Object.assign({}, dynamicComponents);
|
||||
newDCs[name] = component;
|
||||
setDynamicComponents(newDCs);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const hasComponentLoaded = (name: string): boolean =>
|
||||
{
|
||||
return (!!dynamicComponents[name]);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const renderComponent = (name: string, props?: any): JSX.Element =>
|
||||
{
|
||||
if (dynamicComponents[name])
|
||||
{
|
||||
const C: React.FC<DynamicComponentProps> = dynamicComponents[name];
|
||||
return (<C qfmdBridge={qfmdBridge} props={props} />);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<Box>Loading...</Box>);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
loadComponent,
|
||||
hasComponentLoaded,
|
||||
renderComponent
|
||||
};
|
||||
}
|
@ -58,4 +58,6 @@ module.exports = function (app)
|
||||
app.use("/api*", getRequestHandler());
|
||||
app.use("/*api", getRequestHandler());
|
||||
app.use("/qqq/*", getRequestHandler());
|
||||
app.use("/dynamic-qfmd-components/*", getRequestHandler());
|
||||
app.use("/material-dashboard-backend/*", getRequestHandler());
|
||||
};
|
||||
|
@ -181,7 +181,12 @@ public class QBaseSeleniumTest
|
||||
.withRouteToFile("/metaData/table/city", "metaData/table/person.json")
|
||||
.withRouteToFile("/metaData/table/script", "metaData/table/script.json")
|
||||
.withRouteToFile("/metaData/table/scriptRevision", "metaData/table/scriptRevision.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/person", "qqq/v1/metaData/table/person.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/city", "qqq/v1/metaData/table/city.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/script", "qqq/v1/metaData/table/script.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/scriptRevision", "qqq/v1/metaData/table/scriptRevision.json")
|
||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -46,7 +46,9 @@ public class AppPageNavTest extends QBaseSeleniumTest
|
||||
.withRouteToString("/widget/QuickSightChartRenderer", """
|
||||
{"url": "http://www.google.com"}""")
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/city/count", "data/city/count.json");
|
||||
.withRouteToFile("/data/city/count", "data/city/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/city/count", "qqq/v1/table/city/count.json");
|
||||
}
|
||||
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class AssociatedRecordScriptTest extends QBaseSeleniumTest
|
||||
{
|
||||
super.addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/1", "data/person/1701.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/1/developer", "data/person/1701.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/1/developer", "data/person/developer.json");
|
||||
}
|
||||
|
||||
|
||||
|
@ -63,6 +63,8 @@ public class BulkEditTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/variants", "data/person/variants.json");
|
||||
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json");
|
||||
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json");
|
||||
qSeleniumJavalin.withRouteToString("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/records", "[]");
|
||||
}
|
||||
|
||||
|
@ -51,12 +51,15 @@ public class SavedReportTest extends QBaseSeleniumTest
|
||||
super.addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/metaData/table/savedReport", "metaData/table/savedReport.json")
|
||||
.withRouteToFile("/qqq/v1/metaData/table/savedReport", "qqq/v1/metaData/table/savedReport.json")
|
||||
.withRouteToFile("/widget/reportSetupWidget", "widget/reportSetupWidget.json")
|
||||
.withRouteToFile("/widget/pivotTableSetupWidget", "widget/pivotTableSetupWidget.json")
|
||||
.withRouteToFile("/data/savedReport/possibleValues/tableName", "data/savedReport/possibleValues/tableName.json")
|
||||
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
|
||||
;
|
||||
}
|
||||
|
||||
@ -93,8 +96,8 @@ public class SavedReportTest extends QBaseSeleniumTest
|
||||
////////////////////////////////////////////////////
|
||||
qSeleniumJavalin.beginCapture();
|
||||
qSeleniumLib.waitForSelectorContaining("button", "Edit Filters and Columns").click();
|
||||
qSeleniumJavalin.waitForCapturedPath("/data/person/count");
|
||||
qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||
qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
|
||||
qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
|
||||
qSeleniumJavalin.endCapture();
|
||||
|
||||
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||
|
@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlAdvancedModeTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
|
||||
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
|
||||
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||
|
@ -53,6 +53,8 @@ public class QueryScreenFilterInUrlBasicModeTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
|
||||
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
|
||||
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||
|
@ -22,13 +22,16 @@
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
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.QQQMaterialDashboardSelectors;
|
||||
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.QSeleniumJavalin;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
@ -48,6 +51,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin
|
||||
.withRouteToFile("/data/person/count", "data/person/count.json")
|
||||
.withRouteToFile("/data/person/query", "data/person/index.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json")
|
||||
.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json")
|
||||
.withRouteToFile("/data/person/variants", "data/person/variants.json")
|
||||
.withRouteToFile("/data/person/possibleValues/homeCityId", "data/person/possibleValues/homeCityId.json")
|
||||
.withRouteToFile("/processes/querySavedView/init", "processes/querySavedView/init.json");
|
||||
@ -79,8 +84,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
///////////////////////////////////////////////////////////////////
|
||||
String idEquals1FilterSubstring = """
|
||||
{"fieldName":"id","operator":"EQUALS","values":["1"]}""";
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/count", idEquals1FilterSubstring);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", idEquals1FilterSubstring);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/count", idEquals1FilterSubstring);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", idEquals1FilterSubstring);
|
||||
qSeleniumJavalin.endCapture();
|
||||
|
||||
///////////////////////////////////////
|
||||
@ -99,8 +104,8 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// assert that query & count both no longer have the filter value //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/data/person/count");
|
||||
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||
CapturedContext capturedCount = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/count");
|
||||
CapturedContext capturedQuery = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
|
||||
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
||||
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
||||
qSeleniumJavalin.endCapture();
|
||||
@ -132,9 +137,9 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
String expectedFilterContents2 = """
|
||||
"booleanOperator":"OR\"""";
|
||||
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents0);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents0);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents1);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectedFilterContents2);
|
||||
qSeleniumJavalin.endCapture();
|
||||
}
|
||||
|
||||
@ -200,6 +205,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");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -208,7 +411,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin.beginCapture();
|
||||
queryScreenLib.setBasicBooleanFilter(fieldLabel, operatorLabel);
|
||||
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.endCapture();
|
||||
}
|
||||
|
||||
@ -222,7 +425,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
qSeleniumJavalin.beginCapture();
|
||||
queryScreenLib.setBasicFilterPossibleValues(fieldLabel, operatorLabel, values);
|
||||
queryScreenLib.waitForBasicFilterButtonMatchingRegex(expectButtonStringRegex);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.endCapture();
|
||||
}
|
||||
|
||||
@ -268,9 +471,23 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
||||
queryScreenLib.addAdvancedQueryFilterInput(0, fieldLabel, operatorLabel, value, null);
|
||||
qSeleniumLib.clickBackdrop();
|
||||
queryScreenLib.waitForAdvancedQueryStringMatchingRegex(expectQueryStringRegex);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/qqq/v1/table/person/query", expectFilterJsonContains);
|
||||
qSeleniumJavalin.endCapture();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,8 @@ public class SavedViewsTest extends QBaseSeleniumTest
|
||||
super.addJavalinRoutes(qSeleniumJavalin);
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
|
||||
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/count", "qqq/v1/table/person/count.json");
|
||||
qSeleniumJavalin.withRouteToFile("/qqq/v1/table/person/query", "qqq/v1/table/person/index.json");
|
||||
qSeleniumJavalin.withRouteToFile("/data/person/*", "data/person/1701.json");
|
||||
}
|
||||
|
||||
@ -135,7 +137,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
|
||||
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedView/2"));
|
||||
queryScreenLib.assertSavedViewNameOnScreen("Some People");
|
||||
qSeleniumLib.waitForSelectorContaining("DIV", "Unsaved Changes");
|
||||
CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||
CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
|
||||
assertTrue(capturedContext.getBody().contains("Kelkhoff"));
|
||||
qSeleniumJavalin.endCapture();
|
||||
|
||||
@ -162,7 +164,7 @@ public class SavedViewsTest extends QBaseSeleniumTest
|
||||
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
|
||||
qSeleniumLib.waitForCondition("Current URL should not have filter id", () -> !driver.getCurrentUrl().endsWith("/person/savedView/2"));
|
||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "Save View As");
|
||||
capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||
capturedContext = qSeleniumJavalin.waitForCapturedPath("/qqq/v1/table/person/query");
|
||||
assertTrue(capturedContext.getBody().matches("(?s).*id.*LESS_THAN.*10.*"));
|
||||
qSeleniumJavalin.endCapture();
|
||||
}
|
||||
|
166
src/test/resources/fixtures/qqq/v1/metaData/table/person.json
Normal file
166
src/test/resources/fixtures/qqq/v1/metaData/table/person.json
Normal file
@ -0,0 +1,166 @@
|
||||
{
|
||||
"name": "person",
|
||||
"label": "Person",
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "person",
|
||||
"deletePermission": true,
|
||||
"editPermission": true,
|
||||
"insertPermission": true,
|
||||
"readPermission": true,
|
||||
"fields": {
|
||||
"firstName": {
|
||||
"name": "firstName",
|
||||
"label": "First Name",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"lastName": {
|
||||
"name": "lastName",
|
||||
"label": "Last Name",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"annualSalary": {
|
||||
"name": "annualSalary",
|
||||
"label": "Annual Salary",
|
||||
"type": "DECIMAL",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "$%,.2f"
|
||||
},
|
||||
"modifyDate": {
|
||||
"name": "modifyDate",
|
||||
"label": "Modify Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"daysWorked": {
|
||||
"name": "daysWorked",
|
||||
"label": "Days Worked",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%,d"
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"birthDate": {
|
||||
"name": "birthDate",
|
||||
"label": "Birth Date",
|
||||
"type": "DATE",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"isEmployed": {
|
||||
"name": "isEmployed",
|
||||
"label": "Is Employed",
|
||||
"type": "BOOLEAN",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"homeCityId": {
|
||||
"name": "homeCityId",
|
||||
"label": "Home City",
|
||||
"type": "INTEGER",
|
||||
"possibleValueSourceName": "city",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"label": "Email",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"createDate": {
|
||||
"name": "createDate",
|
||||
"label": "Create Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "identity",
|
||||
"label": "Identity",
|
||||
"tier": "T1",
|
||||
"fieldNames": [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName"
|
||||
],
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "basicInfo",
|
||||
"label": "Basic Info",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"email",
|
||||
"birthDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "dataset"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "employmentInfo",
|
||||
"label": "Employment Info",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"isEmployed",
|
||||
"annualSalary",
|
||||
"daysWorked"
|
||||
],
|
||||
"icon": {
|
||||
"name": "work"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "dates",
|
||||
"label": "Dates",
|
||||
"tier": "T3",
|
||||
"fieldNames": [
|
||||
"createDate",
|
||||
"modifyDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "calendar_month"
|
||||
},
|
||||
"isHidden": false
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"TABLE_DELETE",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_UPDATE"
|
||||
]
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
{
|
||||
"name": "savedReport",
|
||||
"label": "Saved Report",
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "article",
|
||||
"fields": {
|
||||
"queryFilterJson": {
|
||||
"name": "queryFilterJson",
|
||||
"label": "Query Filter",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"columnsJson": {
|
||||
"name": "columnsJson",
|
||||
"label": "Columns",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"inputFieldsJson": {
|
||||
"name": "inputFieldsJson",
|
||||
"label": "Input Fields",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"pivotTableJson": {
|
||||
"name": "pivotTableJson",
|
||||
"label": "Pivot Table",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"modifyDate": {
|
||||
"name": "modifyDate",
|
||||
"label": "Modify Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"label": "Report Name",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"label": "User Id",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"tableName": {
|
||||
"name": "tableName",
|
||||
"label": "Table",
|
||||
"type": "STRING",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"possibleValueSourceName": "tables",
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"createDate": {
|
||||
"name": "createDate",
|
||||
"label": "Create Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "identity",
|
||||
"label": "Identity",
|
||||
"tier": "T1",
|
||||
"fieldNames": [
|
||||
"id",
|
||||
"label",
|
||||
"tableName"
|
||||
],
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "filtersAndColumns",
|
||||
"label": "Filters and Columns",
|
||||
"tier": "T2",
|
||||
"widgetName": "reportSetupWidget",
|
||||
"icon": {
|
||||
"name": "table_chart"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "pivotTable",
|
||||
"label": "Pivot Table",
|
||||
"tier": "T2",
|
||||
"widgetName": "pivotTableSetupWidget",
|
||||
"icon": {
|
||||
"name": "pivot_table_chart"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"label": "Data",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"queryFilterJson",
|
||||
"columnsJson",
|
||||
"pivotTableJson"
|
||||
],
|
||||
"icon": {
|
||||
"name": "text_snippet"
|
||||
},
|
||||
"isHidden": true
|
||||
},
|
||||
{
|
||||
"name": "hidden",
|
||||
"label": "Hidden",
|
||||
"tier": "T2",
|
||||
"fieldNames": [
|
||||
"inputFieldsJson",
|
||||
"userId"
|
||||
],
|
||||
"icon": {
|
||||
"name": "text_snippet"
|
||||
},
|
||||
"isHidden": true
|
||||
},
|
||||
{
|
||||
"name": "dates",
|
||||
"label": "Dates",
|
||||
"tier": "T3",
|
||||
"fieldNames": [
|
||||
"createDate",
|
||||
"modifyDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "calendar_month"
|
||||
},
|
||||
"isHidden": false
|
||||
}
|
||||
],
|
||||
"exposedJoins": [],
|
||||
"supplementalTableMetaData": {
|
||||
"materialDashboard": {
|
||||
"fieldRules": [
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "queryFilterJson"
|
||||
},
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "columnsJson"
|
||||
},
|
||||
{
|
||||
"trigger": "ON_CHANGE",
|
||||
"sourceField": "tableName",
|
||||
"action": "CLEAR_TARGET_FIELD",
|
||||
"targetField": "pivotTableJson"
|
||||
}
|
||||
],
|
||||
"type": "materialDashboard"
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"QUERY_STATS",
|
||||
"TABLE_UPDATE",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_DELETE"
|
||||
],
|
||||
"readPermission": true,
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true,
|
||||
"usesVariants": false
|
||||
}
|
137
src/test/resources/fixtures/qqq/v1/metaData/table/script.json
Normal file
137
src/test/resources/fixtures/qqq/v1/metaData/table/script.json
Normal file
@ -0,0 +1,137 @@
|
||||
{
|
||||
"name": "script",
|
||||
"label": "Script",
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "data_object",
|
||||
"fields": {
|
||||
"modifyDate": {
|
||||
"name": "modifyDate",
|
||||
"label": "Modify Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"label": "Name",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"currentScriptRevisionId": {
|
||||
"name": "currentScriptRevisionId",
|
||||
"label": "Current Script Revision",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"possibleValueSourceName": "scriptRevision",
|
||||
"displayFormat": "%s",
|
||||
"adornments": [
|
||||
{
|
||||
"type": "LINK",
|
||||
"values": {
|
||||
"toRecordFromTable": "scriptRevision"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"tableName": {
|
||||
"name": "tableName",
|
||||
"label": "Table Name",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"possibleValueSourceName": "tables",
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"createDate": {
|
||||
"name": "createDate",
|
||||
"label": "Create Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"scriptTypeId": {
|
||||
"name": "scriptTypeId",
|
||||
"label": "Script Type",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"possibleValueSourceName": "scriptType",
|
||||
"displayFormat": "%s",
|
||||
"adornments": [
|
||||
{
|
||||
"type": "LINK",
|
||||
"values": {
|
||||
"toRecordFromTable": "scriptType"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "identity",
|
||||
"label": "Identity",
|
||||
"tier": "T1",
|
||||
"fieldNames": [
|
||||
"id",
|
||||
"name",
|
||||
"scriptTypeId",
|
||||
"tableName",
|
||||
"currentScriptRevisionId"
|
||||
],
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "contents",
|
||||
"label": "Contents",
|
||||
"tier": "T2",
|
||||
"widgetName": "scriptViewer",
|
||||
"icon": {
|
||||
"name": "data_object"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "dates",
|
||||
"label": "Dates",
|
||||
"tier": "T3",
|
||||
"fieldNames": [
|
||||
"createDate",
|
||||
"modifyDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "calendar_month"
|
||||
},
|
||||
"isHidden": false
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_DELETE",
|
||||
"TABLE_UPDATE"
|
||||
],
|
||||
"readPermission": true,
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
{
|
||||
"name": "scriptRevision",
|
||||
"label": "Script Revision",
|
||||
"isHidden": false,
|
||||
"primaryKeyField": "id",
|
||||
"iconName": "history_edu",
|
||||
"fields": {
|
||||
"scriptId": {
|
||||
"name": "scriptId",
|
||||
"label": "Script",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"possibleValueSourceName": "script",
|
||||
"displayFormat": "%s",
|
||||
"adornments": [
|
||||
{
|
||||
"type": "SIZE",
|
||||
"values": {
|
||||
"width": "large"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "LINK",
|
||||
"values": {
|
||||
"toRecordFromTable": "script"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"apiName": {
|
||||
"name": "apiName",
|
||||
"label": "API Name",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"possibleValueSourceName": "apiName",
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"sequenceNo": {
|
||||
"name": "sequenceNo",
|
||||
"label": "Sequence No",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"apiVersion": {
|
||||
"name": "apiVersion",
|
||||
"label": "API Version",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"possibleValueSourceName": "apiVersion",
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"commitMessage": {
|
||||
"name": "commitMessage",
|
||||
"label": "Commit Message",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"modifyDate": {
|
||||
"name": "modifyDate",
|
||||
"label": "Modify Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"author": {
|
||||
"name": "author",
|
||||
"label": "Author",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"id": {
|
||||
"name": "id",
|
||||
"label": "Id",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
"createDate": {
|
||||
"name": "createDate",
|
||||
"label": "Create Date",
|
||||
"type": "DATE_TIME",
|
||||
"isRequired": false,
|
||||
"isEditable": false,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
},
|
||||
"sections": [
|
||||
{
|
||||
"name": "identity",
|
||||
"label": "Identity",
|
||||
"tier": "T1",
|
||||
"fieldNames": [
|
||||
"id",
|
||||
"scriptId",
|
||||
"sequenceNo"
|
||||
],
|
||||
"icon": {
|
||||
"name": "badge"
|
||||
},
|
||||
"isHidden": false
|
||||
},
|
||||
{
|
||||
"name": "dates",
|
||||
"label": "Dates",
|
||||
"tier": "T3",
|
||||
"fieldNames": [
|
||||
"createDate",
|
||||
"modifyDate"
|
||||
],
|
||||
"icon": {
|
||||
"name": "calendar_month"
|
||||
},
|
||||
"isHidden": false
|
||||
}
|
||||
],
|
||||
"exposedJoins": [],
|
||||
"capabilities": [
|
||||
"TABLE_COUNT",
|
||||
"TABLE_GET",
|
||||
"TABLE_QUERY",
|
||||
"TABLE_INSERT",
|
||||
"TABLE_UPDATE",
|
||||
"QUERY_STATS"
|
||||
],
|
||||
"readPermission": true,
|
||||
"insertPermission": true,
|
||||
"editPermission": true,
|
||||
"deletePermission": true,
|
||||
"usesVariants": false
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"records": []
|
||||
}
|
245
src/test/resources/fixtures/qqq/v1/table/audit/query.json
Normal file
245
src/test/resources/fixtures/qqq/v1/table/audit/query.json
Normal file
@ -0,0 +1,245 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 623577,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 278660,
|
||||
"auditDetail.auditId": 623577,
|
||||
"auditDetail.message": "Set First Name to John",
|
||||
"auditDetail.fieldName": "firstName",
|
||||
"auditDetail.newValue": "John"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "623577",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 623577,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 278661,
|
||||
"auditDetail.auditId": 623577,
|
||||
"auditDetail.message": "Removed Doe from Last Name",
|
||||
"auditDetail.fieldName": "lastName",
|
||||
"auditDetail.oldValue": "Doe"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "623577",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 623577,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 278662,
|
||||
"auditDetail.auditId": 623577,
|
||||
"auditDetail.message": "Set Client to ACME",
|
||||
"auditDetail.fieldName": "clientId",
|
||||
"auditDetail.oldValue": "BetaMax",
|
||||
"auditDetail.newValue": "ACME"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "623577",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Inserted",
|
||||
"timestamp": "2023-02-17T14:11:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 624804,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T14:13:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 278990,
|
||||
"auditDetail.auditId": 624804,
|
||||
"auditDetail.message": "Set SLA Expected Service Days to 2",
|
||||
"auditDetail.fieldName": "slaExpectedServiceDays",
|
||||
"auditDetail.newValue": "2"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "624804",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T14:13:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 624804,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T14:13:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 278991,
|
||||
"auditDetail.auditId": 624804,
|
||||
"auditDetail.message": "Set SLA Status to \"Pending\"",
|
||||
"auditDetail.fieldName": "slaStatusId",
|
||||
"auditDetail.newValue": "Pending"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "624804",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T14:13:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 624809,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Audit message here",
|
||||
"timestamp": "2023-02-17T14:13:16Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 279000,
|
||||
"auditDetail.auditId": 624809,
|
||||
"auditDetail.message": "This is a detail message"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "624809",
|
||||
"recordId": "1191682",
|
||||
"message": "Audit message here",
|
||||
"timestamp": "2023-02-17T14:13:16Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 737694,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T17:22:08Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 299222,
|
||||
"auditDetail.auditId": 737694,
|
||||
"auditDetail.message": "Set Estimated Delivery Date Time to 2023-02-18 07:00:00 PM EST",
|
||||
"auditDetail.fieldName": "estimatedDeliveryDateTime",
|
||||
"auditDetail.newValue": "2023-02-18 07:00:00 PM EST"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "737694",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T17:22:08Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 737694,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T17:22:08Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 299223,
|
||||
"auditDetail.auditId": 737694,
|
||||
"auditDetail.message": "Changed Parcel Tracking Status from \"Unknown\" to \"Pre Transit\"",
|
||||
"auditDetail.fieldName": "parcelTrackingStatusId",
|
||||
"auditDetail.oldValue": "Unknown",
|
||||
"auditDetail.newValue": "Pre Transit"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "737694",
|
||||
"recordId": "1191682",
|
||||
"message": "Record was Edited",
|
||||
"timestamp": "2023-02-17T17:22:08Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "audit",
|
||||
"recordLabel": "Parcel 1191682",
|
||||
"values": {
|
||||
"id": 737695,
|
||||
"auditTableId": 4,
|
||||
"auditUserId": 2,
|
||||
"recordId": 1191682,
|
||||
"message": "Updating Parcel based on updated tracking details",
|
||||
"timestamp": "2023-02-17T17:22:09Z",
|
||||
"clientId": 107,
|
||||
"auditDetail.id": 299224,
|
||||
"auditDetail.auditId": 737695,
|
||||
"auditDetail.message": "Set Parcel Tracking Status to Pre Transit based on most recent tracking update: Shipment information sent to FedEx"
|
||||
},
|
||||
"displayValues": {
|
||||
"auditTableId": "Parcel",
|
||||
"auditUserId": "QQQ User",
|
||||
"clientId": "ACME",
|
||||
"id": "737695",
|
||||
"recordId": "1191682",
|
||||
"message": "Updating Parcel based on updated tracking details",
|
||||
"timestamp": "2023-02-17T17:22:09Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
3
src/test/resources/fixtures/qqq/v1/table/city/count.json
Normal file
3
src/test/resources/fixtures/qqq/v1/table/city/count.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"count": 101406
|
||||
}
|
16
src/test/resources/fixtures/qqq/v1/table/person/1701.json
Normal file
16
src/test/resources/fixtures/qqq/v1/table/person/1701.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"tableName": "person",
|
||||
"recordLabel": "John Doe",
|
||||
"values": {
|
||||
"name": "John Doe",
|
||||
"id": 1710,
|
||||
"createDate": "2022-08-30T00:31:00Z",
|
||||
"modifyDate": "2022-08-30T00:31:00Z"
|
||||
},
|
||||
"displayValues": {
|
||||
"name": "John Doe",
|
||||
"id": 1710,
|
||||
"createDate": "2022-08-30T00:31:00Z",
|
||||
"modifyDate": "2022-08-30T00:31:00Z"
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"count": 101406
|
||||
}
|
276
src/test/resources/fixtures/qqq/v1/table/person/developer.json
Normal file
276
src/test/resources/fixtures/qqq/v1/table/person/developer.json
Normal file
@ -0,0 +1,276 @@
|
||||
{
|
||||
"record": {
|
||||
"tableName": "client",
|
||||
"recordLabel": "John Doe",
|
||||
"values": {
|
||||
"name": "John Doe",
|
||||
"id": 120,
|
||||
"deposcoOrderOptimizationCoolingScriptId": 2,
|
||||
"createDate": "2022-08-30T00:31:00Z",
|
||||
"modifyDate": "2023-02-19T01:28:30Z",
|
||||
"isFulfillmentCenter": false,
|
||||
"infoplusLobId": 18698,
|
||||
"deposcoBusinessUnitName": "TRIFECTA",
|
||||
"deposcoBusinessUnitId": 77,
|
||||
"optimizationConfigId": 1,
|
||||
"nfCode": "Client 224"
|
||||
},
|
||||
"displayValues": {
|
||||
"optimizationConfigId": "Client: 120",
|
||||
"name": "John Doe",
|
||||
"id": "120",
|
||||
"deposcoOrderOptimizationCoolingScriptId": "2",
|
||||
"createDate": "2022-08-30T00:31:00Z",
|
||||
"modifyDate": "2023-02-19T01:28:30Z",
|
||||
"isFulfillmentCenter": "No",
|
||||
"infoplusLobId": "18698",
|
||||
"deposcoBusinessUnitName": "TRIFECTA",
|
||||
"deposcoBusinessUnitId": "77",
|
||||
"nfCode": "Client 224"
|
||||
}
|
||||
},
|
||||
"associatedScripts": [
|
||||
{
|
||||
"testInputFields": [
|
||||
{
|
||||
"name": "selectedTimeInTransitDays",
|
||||
"label": "Selected Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "standardTimeInTransitDays",
|
||||
"label": "Standard Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"scriptType": {
|
||||
"tableName": "scriptType",
|
||||
"values": {
|
||||
"name": "Deposco Order Optimization Cooling",
|
||||
"id": 2,
|
||||
"createDate": "2022-10-31T19:06:50Z",
|
||||
"modifyDate": "2022-10-31T19:06:50Z"
|
||||
}
|
||||
},
|
||||
"scriptRevisions": [
|
||||
{
|
||||
"tableName": "scriptRevision",
|
||||
"values": {
|
||||
"id": 1,
|
||||
"contents": "1;",
|
||||
"createDate": "2023-02-19T01:28:30Z",
|
||||
"modifyDate": "2023-02-19T01:28:30Z",
|
||||
"scriptId": 2,
|
||||
"sequenceNo": 1,
|
||||
"commitMessage": "Initial version",
|
||||
"author": "Darin Kelkhoff"
|
||||
}
|
||||
}
|
||||
],
|
||||
"testOutputFields": [
|
||||
{
|
||||
"name": "sku",
|
||||
"label": "Sku",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "quantityPerCarton",
|
||||
"label": "Quantity Per Carton",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "useClientProvidedCoolingSolution",
|
||||
"label": "Use Client Provided Cooling Solution",
|
||||
"type": "BOOLEAN",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "reason",
|
||||
"label": "Reason",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"script": {
|
||||
"tableName": "script",
|
||||
"values": {
|
||||
"name": "John Doe - Deposco Order Optimization Cooling",
|
||||
"id": 2,
|
||||
"scriptTypeId": 2,
|
||||
"createDate": "2023-02-19T01:28:30Z",
|
||||
"modifyDate": "2023-02-19T01:28:30Z",
|
||||
"currentScriptRevisionId": 1
|
||||
}
|
||||
},
|
||||
"associatedScript": {
|
||||
"fieldName": "deposcoOrderOptimizationCoolingScriptId",
|
||||
"scriptTypeId": 2,
|
||||
"scriptTester": {
|
||||
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationCoolingScript",
|
||||
"codeType": "JAVA",
|
||||
"codeUsage": "SCRIPT_TESTER"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"testInputFields": [
|
||||
{
|
||||
"name": "selectedTimeInTransitDays",
|
||||
"label": "Selected Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "standardTimeInTransitDays",
|
||||
"label": "Standard Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "runtimeWeekday",
|
||||
"label": "Runtime Weekday",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"scriptType": {
|
||||
"tableName": "scriptType",
|
||||
"values": {
|
||||
"name": "Deposco Order Optimization Batch Name",
|
||||
"id": 1,
|
||||
"createDate": "2022-10-31T19:06:50Z",
|
||||
"modifyDate": "2022-10-31T19:06:50Z"
|
||||
}
|
||||
},
|
||||
"testOutputFields": [
|
||||
{
|
||||
"name": "batchName",
|
||||
"label": "Batch Name",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "reason",
|
||||
"label": "Reason",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"associatedScript": {
|
||||
"fieldName": "deposcoOrderOptimizationBatchNameScriptId",
|
||||
"scriptTypeId": 1,
|
||||
"scriptTester": {
|
||||
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationBatchNameScript",
|
||||
"codeType": "JAVA",
|
||||
"codeUsage": "SCRIPT_TESTER"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"testInputFields": [
|
||||
{
|
||||
"name": "selectedTimeInTransitDays",
|
||||
"label": "Selected Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "standardTimeInTransitDays",
|
||||
"label": "Standard Time In Transit Days",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "runtimeWeekday",
|
||||
"label": "Runtime Weekday",
|
||||
"type": "INTEGER",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"scriptType": {
|
||||
"tableName": "scriptType",
|
||||
"values": {
|
||||
"name": "Deposco Order Optimization Batch Name",
|
||||
"id": 1,
|
||||
"createDate": "2022-10-31T19:06:50Z",
|
||||
"modifyDate": "2022-10-31T19:06:50Z"
|
||||
}
|
||||
},
|
||||
"testOutputFields": [
|
||||
{
|
||||
"name": "batchName",
|
||||
"label": "Batch Name",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
},
|
||||
{
|
||||
"name": "reason",
|
||||
"label": "Reason",
|
||||
"type": "STRING",
|
||||
"isRequired": false,
|
||||
"isEditable": true,
|
||||
"isHeavy": false,
|
||||
"displayFormat": "%s"
|
||||
}
|
||||
],
|
||||
"associatedScript": {
|
||||
"fieldName": "deposcoOrderOptimizationCartonizationScriptId",
|
||||
"scriptTypeId": 1,
|
||||
"scriptTester": {
|
||||
"name": "com.coldtrack.live.processes.deposco.RunDeposcoOrderOptimizationBatchNameScript",
|
||||
"codeType": "JAVA",
|
||||
"codeUsage": "SCRIPT_TESTER"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
64
src/test/resources/fixtures/qqq/v1/table/person/index.json
Normal file
64
src/test/resources/fixtures/qqq/v1/table/person/index.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"tableName": "person",
|
||||
"values": {
|
||||
"id": 1,
|
||||
"createDate": "2022-07-23T00:17:00",
|
||||
"modifyDate": "2022-07-22T19:17:06",
|
||||
"firstName": "Jonny",
|
||||
"lastName": "Doe",
|
||||
"birthDate": "1980-05-31",
|
||||
"email": "jdoe@kingsrook.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "person",
|
||||
"values": {
|
||||
"id": 2,
|
||||
"createDate": "2022-07-23T00:17:00",
|
||||
"modifyDate": "2022-07-23T00:17:00",
|
||||
"firstName": "James",
|
||||
"lastName": "Maes",
|
||||
"birthDate": "1980-05-15",
|
||||
"email": "jmaes@mmltholdings.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "person",
|
||||
"values": {
|
||||
"id": 3,
|
||||
"createDate": "2022-07-23T00:17:00",
|
||||
"modifyDate": "2022-07-23T00:17:00",
|
||||
"firstName": "Tim",
|
||||
"lastName": "Chamberlain",
|
||||
"birthDate": "1976-05-28",
|
||||
"email": "tchamberlain@mmltholdings.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "person",
|
||||
"values": {
|
||||
"id": 4,
|
||||
"createDate": "2022-07-23T00:17:00",
|
||||
"modifyDate": "2022-07-23T00:17:00",
|
||||
"firstName": "Tyler",
|
||||
"lastName": "Samples",
|
||||
"birthDate": "1986-05-28",
|
||||
"email": "tsamples@mmltholdings.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "person",
|
||||
"values": {
|
||||
"id": 5,
|
||||
"createDate": "2022-07-23T00:17:00",
|
||||
"modifyDate": "2022-07-23T00:17:00",
|
||||
"firstName": "Garret",
|
||||
"lastName": "Richardson",
|
||||
"birthDate": "1981-01-01",
|
||||
"email": "grichardson@mmltholdings.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "St. Louis"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "Chesterfield"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"id": 1,
|
||||
"label": "St. Louis"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
[]
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"id": "person",
|
||||
"label": "Person"
|
||||
},
|
||||
{
|
||||
"id": "city",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"id": "savedReport",
|
||||
"label": "Saved Report"
|
||||
}
|
||||
]
|
||||
}
|
22
src/test/resources/fixtures/qqq/v1/table/script/1.json
Normal file
22
src/test/resources/fixtures/qqq/v1/table/script/1.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"tableName": "script",
|
||||
"recordLabel": "Hello, Script",
|
||||
"values": {
|
||||
"name": "Hello, Script",
|
||||
"id": 1,
|
||||
"currentScriptRevisionId": 100,
|
||||
"tableName": "client",
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z",
|
||||
"scriptTypeId": 1
|
||||
},
|
||||
"displayValues": {
|
||||
"tableName": "Client",
|
||||
"scriptTypeId": "Record Script",
|
||||
"name": "Hello, Script",
|
||||
"currentScriptRevisionId": 100,
|
||||
"id": "1",
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z"
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"records": []
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
{
|
||||
"tableName": "scriptRevision",
|
||||
"recordLabel": "Hello, Script Revision",
|
||||
"values": {
|
||||
"id": "100",
|
||||
"name": "Hello, Script Revision",
|
||||
"sequenceNo": "22",
|
||||
"commitMessage": "Initial checkin",
|
||||
"author": "Jon Programmer",
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z"
|
||||
},
|
||||
"displayValues": {
|
||||
"id": "1",
|
||||
"name": "Hello, Script Revision",
|
||||
"scriptId": "1",
|
||||
"sequenceNo": "22",
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z"
|
||||
},
|
||||
"associatedRecords": {
|
||||
"files": [
|
||||
{
|
||||
"tableName": "scriptRevisionFile",
|
||||
"values": {
|
||||
"id": 101,
|
||||
"fileName": "Script.js",
|
||||
"contents": "var hello;",
|
||||
"scriptRevisionId": 100,
|
||||
"createDate": "2023-06-23T21:59:57Z",
|
||||
"modifyDate": "2023-06-23T21:59:57Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"tableName": "scriptRevision",
|
||||
"values": {
|
||||
"contents": "var hello;",
|
||||
"id": 100,
|
||||
"sequenceNo": 2,
|
||||
"commitMessage": "2nd commit",
|
||||
"author": "Jon Programmer",
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z"
|
||||
},
|
||||
"displayValues": {
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "scriptRevision",
|
||||
"values": {
|
||||
"contents": "var goodBye;",
|
||||
"id": 99,
|
||||
"sequenceNo": 1,
|
||||
"commitMessage": "Initial checkin",
|
||||
"author": "Jane Programmer",
|
||||
"createDate": "2023-02-17T00:47:51Z",
|
||||
"modifyDate": "2023-02-17T00:47:51Z"
|
||||
},
|
||||
"displayValues": {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
13
src/test/resources/fixtures/qqq/v1/table/scriptType/1.json
Normal file
13
src/test/resources/fixtures/qqq/v1/table/scriptType/1.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"tableName": "scriptType",
|
||||
"recordLabel": "Record Script",
|
||||
"values": {
|
||||
"name": "Record Script",
|
||||
"id": 1,
|
||||
"createDate": "2023-02-18T00:47:51Z",
|
||||
"modifyDate": "2023-02-18T00:47:51Z",
|
||||
"fileMode": 1
|
||||
},
|
||||
"displayValues": {
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user