mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
1 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
575ffe761f |
@ -2,7 +2,7 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@5.1.0
|
||||
browser-tools: circleci/browser-tools@1.4.7
|
||||
browser-tools: circleci/browser-tools@1.4.5
|
||||
|
||||
executors:
|
||||
java17:
|
||||
|
@ -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.86",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.82",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
@ -42,7 +42,6 @@
|
||||
"react-dom": "18.0.0",
|
||||
"react-github-btn": "1.2.1",
|
||||
"react-google-drive-picker": "^1.2.0",
|
||||
"react-markdown": "9.0.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.20.0-SNAPSHOT</revision>
|
||||
<revision>0.19.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@ -66,7 +66,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>feature-CE-876-develop-missing-widget-types-20240221.002945-1</version>
|
||||
<version>0.17.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@ -77,13 +77,13 @@
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.15.0</version>
|
||||
<version>4.10.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.bonigarcia</groupId>
|
||||
<artifactId>webdrivermanager</artifactId>
|
||||
<version>5.6.2</version>
|
||||
<version>5.4.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@ -119,7 +119,7 @@
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
<version>20230227</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
35
src/App.tsx
35
src/App.tsx
@ -36,7 +36,7 @@ import {LicenseInfo} from "@mui/x-license-pro";
|
||||
import jwt_decode from "jwt-decode";
|
||||
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
|
||||
import {useCookies} from "react-cookie";
|
||||
import {Navigate, Route, Routes, useLocation, useSearchParams,} from "react-router-dom";
|
||||
import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
|
||||
import {Md5} from "ts-md5/dist/md5";
|
||||
import CommandMenu from "CommandMenu";
|
||||
import QContext from "QContext";
|
||||
@ -73,14 +73,6 @@ export default function App()
|
||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// 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])
|
||||
@ -175,8 +167,18 @@ export default function App()
|
||||
console.log("Using existing sessionUUID cookie");
|
||||
}
|
||||
|
||||
/*
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
|
||||
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||
localStorage.removeItem("accessToken");
|
||||
*/
|
||||
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
|
||||
|
||||
setLoggedInUser(user);
|
||||
console.log("Token load complete.");
|
||||
@ -197,8 +199,8 @@ export default function App()
|
||||
// use a random token if anonymous or mock //
|
||||
/////////////////////////////////////////////
|
||||
console.log("Generating random token...");
|
||||
qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
|
||||
setIsFullyAuthenticated(true);
|
||||
qController.setGotAuthentication();
|
||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||
console.log("Token generation complete.");
|
||||
return;
|
||||
@ -226,7 +228,6 @@ export default function App()
|
||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||
const {pathname} = useLocation();
|
||||
const [queryParams] = useSearchParams();
|
||||
|
||||
const [needToLoadRoutes, setNeedToLoadRoutes] = useState(true);
|
||||
const [sideNavRoutes, setSideNavRoutes] = useState([]);
|
||||
@ -355,7 +356,7 @@ export default function App()
|
||||
routeList.push({
|
||||
name: `${app.label}`,
|
||||
key: app.name,
|
||||
route: `${path}/savedView/:id`,
|
||||
route: `${path}/savedFilter/:id`,
|
||||
component: <RecordQuery table={table} key={table.name} />,
|
||||
});
|
||||
|
||||
@ -518,7 +519,7 @@ export default function App()
|
||||
name: loggedInUser?.name ?? "Anonymous",
|
||||
key: "username",
|
||||
noCollapse: true,
|
||||
icon: <Avatar src={profilePicture} alt="{loggedInUser?.name}" />,
|
||||
icon: <Avatar src={profilePicture} alt="{user?.name}" />,
|
||||
};
|
||||
setProfileRoutes(profileRoutes);
|
||||
|
||||
@ -656,28 +657,22 @@ export default function App()
|
||||
|
||||
const [pageHeader, setPageHeader] = useState("" as string | JSX.Element);
|
||||
const [accentColor, setAccentColor] = useState("#0062FF");
|
||||
const [accentColorLight, setAccentColorLight] = useState("#C0D6F7")
|
||||
const [tableMetaData, setTableMetaData] = useState(null);
|
||||
const [tableProcesses, setTableProcesses] = useState(null);
|
||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||
|
||||
return (
|
||||
|
||||
appRoutes && (
|
||||
<QContext.Provider value={{
|
||||
pageHeader: pageHeader,
|
||||
accentColor: accentColor,
|
||||
accentColorLight: accentColorLight,
|
||||
tableMetaData: tableMetaData,
|
||||
tableProcesses: tableProcesses,
|
||||
dotMenuOpen: dotMenuOpen,
|
||||
keyboardHelpOpen: keyboardHelpOpen,
|
||||
helpHelpActive: helpHelpActive,
|
||||
setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
|
||||
setAccentColor: (accentColor: string) => setAccentColor(accentColor),
|
||||
setAccentColorLight: (accentColorLight: string) => setAccentColorLight(accentColorLight),
|
||||
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
|
||||
setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses),
|
||||
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent),
|
||||
@ -707,4 +702,4 @@ export default function App()
|
||||
</QContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData";
|
||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||
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 {createContext} from "react";
|
||||
@ -30,10 +32,7 @@ interface QContext
|
||||
setPageHeader?: (header: string | JSX.Element) => void;
|
||||
|
||||
accentColor: string;
|
||||
setAccentColor?: (color: string) => void;
|
||||
|
||||
accentColorLight: string;
|
||||
setAccentColorLight?: (color: string) => void;
|
||||
setAccentColor?: (header: string) => void;
|
||||
|
||||
dotMenuOpen: boolean;
|
||||
setDotMenuOpen?: (dotMenuOpen: boolean) => void;
|
||||
@ -52,17 +51,14 @@ interface QContext
|
||||
///////////////////////////////////
|
||||
pathToLabelMap?: {[path: string]: string};
|
||||
branding?: QBrandingMetaData;
|
||||
helpHelpActive?: boolean;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
pageHeader: "",
|
||||
accentColor: "#0062FF",
|
||||
accentColorLight: "#C0D6F7",
|
||||
dotMenuOpen: false,
|
||||
keyboardHelpOpen: false,
|
||||
pathToLabelMap: {},
|
||||
helpHelpActive: false,
|
||||
};
|
||||
|
||||
const QContext = createContext<QContext>(defaultState);
|
||||
|
10
src/main/java/Placeholder.java
Executable file
10
src/main/java/Placeholder.java
Executable file
@ -0,0 +1,10 @@
|
||||
/*******************************************************************************
|
||||
** Placeholder class, because maven really wants some source under src/main?
|
||||
*******************************************************************************/
|
||||
public class Placeholder
|
||||
{
|
||||
public void f()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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 com.kingsrook.qqq.backend.core.model.metadata.layout.QSupplementalAppMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** app-level meta-data for this module (handled as QSupplementalTableMetaData)
|
||||
*******************************************************************************/
|
||||
public class MaterialDashboardAppMetaData extends QSupplementalAppMetaData
|
||||
{
|
||||
private Boolean showAppLabelOnHomeScreen = true;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean includeInFullFrontendMetaData()
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return ("materialDashboard");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for showAppLabelOnHomeScreen
|
||||
*******************************************************************************/
|
||||
public Boolean getShowAppLabelOnHomeScreen()
|
||||
{
|
||||
return (this.showAppLabelOnHomeScreen);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for showAppLabelOnHomeScreen
|
||||
*******************************************************************************/
|
||||
public void setShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen)
|
||||
{
|
||||
this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for showAppLabelOnHomeScreen
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardAppMetaData withShowAppLabelOnHomeScreen(Boolean showAppLabelOnHomeScreen)
|
||||
{
|
||||
this.showAppLabelOnHomeScreen = showAppLabelOnHomeScreen;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface MaterialDashboardIconRoleNames
|
||||
{
|
||||
String TOP_RIGHT_INSIDE_CARD = "topRightInsideCard";
|
||||
String TOP_LEFT_INSIDE_CARD = "topLeftInsideCard";
|
||||
}
|
@ -22,23 +22,17 @@
|
||||
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** table-level meta-data for this module (handled as QSupplementalTableMetaData)
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
|
||||
{
|
||||
private List<List<String>> gotoFieldNames;
|
||||
private List<String> defaultQuickFilterFieldNames;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -92,73 +86,4 @@ public class MaterialDashboardTableMetaData extends QSupplementalTableMetaData
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
super.validate(qInstance, tableMetaData, qInstanceValidator);
|
||||
|
||||
String prefix = "MaterialDashboardTableMetaData supplementalTableMetaData for table [" + tableMetaData.getName() + "] ";
|
||||
|
||||
for(List<String> gotoFieldNameSubList : CollectionUtils.nonNullList(gotoFieldNames))
|
||||
{
|
||||
qInstanceValidator.assertCondition(!gotoFieldNameSubList.isEmpty(), prefix + "has an empty gotoFieldNames list");
|
||||
validateListOfFieldNames(tableMetaData, gotoFieldNameSubList, qInstanceValidator, prefix + "gotoFieldNames: ");
|
||||
}
|
||||
validateListOfFieldNames(tableMetaData, defaultQuickFilterFieldNames, qInstanceValidator, prefix + "defaultQuickFilterFieldNames: ");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateListOfFieldNames(QTableMetaData tableMetaData, List<String> fieldNames, QInstanceValidator qInstanceValidator, String prefix)
|
||||
{
|
||||
Set<String> usedNames = new HashSet<>();
|
||||
for(String fieldName : CollectionUtils.nonNullList(fieldNames))
|
||||
{
|
||||
if(qInstanceValidator.assertNoException(() -> tableMetaData.getField(fieldName), prefix + " unrecognized field name: " + fieldName))
|
||||
{
|
||||
qInstanceValidator.assertCondition(!usedNames.contains(fieldName), prefix + " has a duplicated field name: " + fieldName);
|
||||
usedNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultQuickFilterFieldNames
|
||||
*******************************************************************************/
|
||||
public List<String> getDefaultQuickFilterFieldNames()
|
||||
{
|
||||
return (this.defaultQuickFilterFieldNames);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for defaultQuickFilterFieldNames
|
||||
*******************************************************************************/
|
||||
public void setDefaultQuickFilterFieldNames(List<String> defaultQuickFilterFieldNames)
|
||||
{
|
||||
this.defaultQuickFilterFieldNames = defaultQuickFilterFieldNames;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for defaultQuickFilterFieldNames
|
||||
*******************************************************************************/
|
||||
public MaterialDashboardTableMetaData withDefaultQuickFilterFieldNames(List<String> defaultQuickFilterFieldNames)
|
||||
{
|
||||
this.defaultQuickFilterFieldNames = defaultQuickFilterFieldNames;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ interface Types {
|
||||
}
|
||||
|
||||
const baseProperties = {
|
||||
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontWeightLighter: 100,
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
|
@ -78,19 +78,6 @@ interface Types
|
||||
light: string;
|
||||
main: string;
|
||||
focus: string;
|
||||
}
|
||||
blueGray:
|
||||
| {
|
||||
main: string;
|
||||
}
|
||||
gray:
|
||||
| {
|
||||
main: string;
|
||||
focus: string;
|
||||
}
|
||||
grayLines:
|
||||
| {
|
||||
main: string;
|
||||
}
|
||||
| any;
|
||||
primary: ColorsTypes | any;
|
||||
@ -187,19 +174,6 @@ const colors: Types = {
|
||||
focus: "#ffffff",
|
||||
},
|
||||
|
||||
blueGray: {
|
||||
main: "#546E7A"
|
||||
},
|
||||
|
||||
gray: {
|
||||
main: "#757575",
|
||||
focus: "#757575",
|
||||
},
|
||||
|
||||
grayLines: {
|
||||
main: "#D6D6D6"
|
||||
},
|
||||
|
||||
black: {
|
||||
light: "#000000",
|
||||
main: "#000000",
|
||||
@ -242,7 +216,7 @@ const colors: Types = {
|
||||
},
|
||||
|
||||
dark: {
|
||||
main: "#212121",
|
||||
main: "#344767",
|
||||
focus: "#2c3c58",
|
||||
},
|
||||
|
||||
|
@ -149,7 +149,7 @@ interface Types {
|
||||
}
|
||||
|
||||
const baseProperties = {
|
||||
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontWeightLighter: 100,
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
@ -199,10 +199,9 @@ const typography: Types = {
|
||||
},
|
||||
|
||||
h3: {
|
||||
fontSize: "1.75rem",
|
||||
fontSize: pxToRem(30),
|
||||
lineHeight: 1.375,
|
||||
...baseHeadingProperties,
|
||||
fontWeight: 600
|
||||
},
|
||||
|
||||
h4: {
|
||||
@ -218,10 +217,9 @@ const typography: Types = {
|
||||
},
|
||||
|
||||
h6: {
|
||||
fontSize: "1.125rem",
|
||||
fontSize: pxToRem(16),
|
||||
lineHeight: 1.625,
|
||||
...baseHeadingProperties,
|
||||
fontWeight: 500
|
||||
},
|
||||
|
||||
subtitle1: {
|
||||
|
@ -31,7 +31,7 @@ type Types = any;
|
||||
|
||||
const card: Types = {
|
||||
defaultProps: {
|
||||
elevation: 0
|
||||
elevation: 3
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@ -42,7 +42,7 @@ const card: Types = {
|
||||
wordWrap: "break-word",
|
||||
backgroundColor: white.main,
|
||||
backgroundClip: "border-box",
|
||||
border: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
border: `${borderWidth[0]} solid ${rgba(black.main, 0.125)}`,
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: "visible",
|
||||
},
|
||||
|
@ -1,75 +1,68 @@
|
||||
/**
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
|
||||
Coded by www.creative-tim.com
|
||||
Coded by www.creative-tim.com
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import borders from "qqq/assets/theme/base/borders";
|
||||
import boxShadows from "qqq/assets/theme/base/boxShadows";
|
||||
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const {grey, white} = colors;
|
||||
const { grey, white } = colors;
|
||||
const { borderRadius } = borders;
|
||||
const { tabsBoxShadow } = boxShadows;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
|
||||
const tabs: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "relative",
|
||||
borderRadius: 0,
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: grey[400],
|
||||
minHeight: "unset",
|
||||
padding: "0",
|
||||
margin: "0",
|
||||
"& button": {
|
||||
fontWeight: 500
|
||||
}
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
position: "relative",
|
||||
backgroundColor: grey[100],
|
||||
borderRadius: borderRadius.xl,
|
||||
minHeight: "unset",
|
||||
padding: pxToRem(4),
|
||||
},
|
||||
|
||||
scroller: {
|
||||
marginLeft: "0.5rem"
|
||||
},
|
||||
flexContainer: {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
zIndex: 10,
|
||||
},
|
||||
|
||||
flexContainer: {
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
width: "fit-content",
|
||||
zIndex: 10,
|
||||
},
|
||||
fixed: {
|
||||
overflow: "unset !important",
|
||||
overflowX: "unset !important",
|
||||
},
|
||||
|
||||
fixed: {
|
||||
overflow: "unset !important",
|
||||
overflowX: "unset !important",
|
||||
vertical: {
|
||||
"& .MuiTabs-indicator": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
vertical: {
|
||||
"& .MuiTabs-indicator": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
indicator: {
|
||||
height: "100%",
|
||||
borderRadius: 0,
|
||||
backgroundColor: white.main,
|
||||
borderBottom: "2px solid",
|
||||
borderBottomColor: colors.info.main,
|
||||
transition: "all 500ms ease",
|
||||
},
|
||||
},
|
||||
indicator: {
|
||||
height: "100%",
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: white.main,
|
||||
boxShadow: tabsBoxShadow.indicator,
|
||||
transition: "all 500ms ease",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default tabs;
|
||||
|
@ -1,17 +1,17 @@
|
||||
/**
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
=========================================================
|
||||
* Material Dashboard 2 PRO React TS - v1.0.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
|
||||
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
|
||||
|
||||
Coded by www.creative-tim.com
|
||||
Coded by www.creative-tim.com
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*/
|
||||
|
||||
// Material Dashboard 2 PRO React TS Base Styles
|
||||
import typography from "qqq/assets/theme/base/typography";
|
||||
@ -21,50 +21,48 @@ import colors from "qqq/assets/theme/base/colors";
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const {size, fontWeightRegular} = typography;
|
||||
const {borderRadius} = borders;
|
||||
const {dark} = colors;
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { borderRadius } = borders;
|
||||
const { dark } = colors;
|
||||
|
||||
// types
|
||||
type Types = any;
|
||||
|
||||
const tab: Types = {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
flex: "1 1 auto",
|
||||
textAlign: "center",
|
||||
maxWidth: "unset !important",
|
||||
minWidth: "unset !important",
|
||||
minHeight: "unset !important",
|
||||
fontSize: size.md,
|
||||
fontWeight: fontWeightRegular,
|
||||
textTransform: "none",
|
||||
lineHeight: "inherit",
|
||||
padding: "0.75rem 0.5rem 0.5rem",
|
||||
margin: "0 0.5rem",
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
color: `${dark.main} !important`,
|
||||
opacity: "1 !important",
|
||||
styleOverrides: {
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
flex: "1 1 auto",
|
||||
textAlign: "center",
|
||||
maxWidth: "unset !important",
|
||||
minWidth: "unset !important",
|
||||
minHeight: "unset !important",
|
||||
fontSize: size.md,
|
||||
fontWeight: fontWeightRegular,
|
||||
textTransform: "none",
|
||||
lineHeight: "inherit",
|
||||
padding: pxToRem(4),
|
||||
borderRadius: borderRadius.lg,
|
||||
color: `${dark.main} !important`,
|
||||
opacity: "1 !important",
|
||||
|
||||
"& .material-icons, .material-icons-round": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
|
||||
"& svg": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
"& .material-icons, .material-icons-round": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
|
||||
labelIcon: {
|
||||
paddingTop: pxToRem(4),
|
||||
"& svg": {
|
||||
marginBottom: "0 !important",
|
||||
marginRight: pxToRem(6),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
labelIcon: {
|
||||
paddingTop: pxToRem(4),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default tab;
|
||||
|
@ -24,7 +24,7 @@ import borders from "qqq/assets/theme/base/borders";
|
||||
// Material Dashboard 2 PRO React TS Helper Functions
|
||||
import pxToRem from "qqq/assets/theme/functions/pxToRem";
|
||||
|
||||
const { black, light, white, dark } = colors;
|
||||
const { black, light } = colors;
|
||||
const { size, fontWeightRegular } = typography;
|
||||
const { borderRadius } = borders;
|
||||
|
||||
@ -39,20 +39,19 @@ const tooltip: Types = {
|
||||
|
||||
styleOverrides: {
|
||||
tooltip: {
|
||||
maxWidth: pxToRem(300),
|
||||
backgroundColor: white.main,
|
||||
color: dark.main,
|
||||
maxWidth: pxToRem(200),
|
||||
backgroundColor: black.main,
|
||||
color: light.main,
|
||||
fontSize: size.sm,
|
||||
fontWeight: fontWeightRegular,
|
||||
textAlign: "left",
|
||||
textAlign: "center",
|
||||
borderRadius: borderRadius.md,
|
||||
opacity: 0.7,
|
||||
padding: "1rem",
|
||||
boxShadow: "0px 0px 12px rgba(128, 128, 128, 0.40)"
|
||||
padding: `${pxToRem(5)} ${pxToRem(8)} ${pxToRem(4)}`,
|
||||
},
|
||||
|
||||
arrow: {
|
||||
color: white.main,
|
||||
color: black.main,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -402,16 +402,14 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<Box key={audit0.values.get("id")} className="auditGroupBlock">
|
||||
<Box position="sticky" top="0" zIndex={3}>
|
||||
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14} position={"relative"} top={"-1px"} pb={"6px"} sx={{backgroundImage: "linear-gradient(to bottom, rgba(255,255,255,1), rgba(255,255,255,1) 80%, rgba(255,255,255,0))"}}>
|
||||
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
|
||||
<Box whiteSpace="nowrap">
|
||||
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
|
||||
{timestampParts[0] == todayFormatted ? " (Today)" : ""}
|
||||
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""}
|
||||
</Box>
|
||||
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
|
||||
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14}>
|
||||
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
|
||||
<Box whiteSpace="nowrap">
|
||||
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
|
||||
{timestampParts[0] == todayFormatted ? " (Today)" : ""}
|
||||
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""}
|
||||
</Box>
|
||||
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
|
||||
</Box>
|
||||
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ interface QCreateNewButtonProps
|
||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box display="inline-block" ml={3} mr={0} width={standardWidth}>
|
||||
<Box ml={3} mr={2} width={standardWidth}>
|
||||
<Link to={`${tablePath}/create`}>
|
||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||
Create New
|
||||
@ -73,17 +73,13 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
||||
interface QDeleteButtonProps
|
||||
{
|
||||
onClickHandler: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
QDeleteButton.defaultProps = {
|
||||
disabled: false
|
||||
};
|
||||
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
||||
export function QDeleteButton({onClickHandler}: QDeleteButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>}>
|
||||
Delete
|
||||
</MDButton>
|
||||
</Box>
|
||||
@ -127,6 +123,24 @@ export function QActionsMenuButton({isOpen, onClickHandler}: QActionsMenuButtonP
|
||||
);
|
||||
}
|
||||
|
||||
export function QSavedFiltersMenuButton({isOpen, onClickHandler}: QActionsMenuButtonProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box width={standardWidth} ml={1}>
|
||||
<MDButton
|
||||
variant={isOpen ? "contained" : "outlined"}
|
||||
color="dark"
|
||||
onClick={onClickHandler}
|
||||
fullWidth
|
||||
startIcon={<Icon>filter_alt</Icon>}
|
||||
>
|
||||
saved filters
|
||||
<Icon>keyboard_arrow_down</Icon>
|
||||
</MDButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface QCancelButtonProps
|
||||
{
|
||||
onClickHandler: any;
|
||||
|
@ -19,18 +19,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Box, InputLabel} from "@mui/material";
|
||||
import {InputLabel} from "@mui/material";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {styled} from "@mui/material/styles";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {useFormikContext} from "formik";
|
||||
import React, {SyntheticEvent} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
width: 32,
|
||||
height: 20,
|
||||
width: 28,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
display: "flex",
|
||||
"&:active": {
|
||||
@ -54,19 +53,15 @@ const AntSwitch = styled(Switch)(({theme}) => ({
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)",
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
transition: theme.transitions.create([ "width" ], {
|
||||
duration: 200,
|
||||
}),
|
||||
},
|
||||
"&.nullSwitch .MuiSwitch-thumb": {
|
||||
width: 28,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 20 / 2,
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
backgroundColor:
|
||||
theme.palette.mode === "dark" ? "rgba(255,255,255,.35)" : "rgba(0,0,0,.25)",
|
||||
@ -83,7 +78,6 @@ interface Props
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Element
|
||||
{
|
||||
const {setFieldValue} = useFormikContext();
|
||||
@ -102,29 +96,27 @@ function BooleanFieldSwitch({name, label, value, isDisabled}: Props) : JSX.Eleme
|
||||
setFieldValue(name, !value);
|
||||
}
|
||||
|
||||
const classNullSwitch = (value === null || value == undefined || `${value}` == "") ? "nullSwitch" : "";
|
||||
|
||||
return (
|
||||
<Box bgcolor={isDisabled ? colors.grey[200] : ""}>
|
||||
<>
|
||||
<InputLabel shrink={true}>{label}</InputLabel>
|
||||
<Stack direction="row" spacing={1} alignItems="center" height="37px">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography
|
||||
fontSize="1rem"
|
||||
fontSize="0.875rem"
|
||||
color={value === false ? "auto" : "#bfbfbf" }
|
||||
onClick={(e) => setSwitch(e, false)}
|
||||
sx={{cursor: value === false || isDisabled ? "inherit" : "pointer"}}>
|
||||
No
|
||||
</Typography>
|
||||
<AntSwitch className={classNullSwitch} name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<AntSwitch name={name} checked={value} onClick={toggleSwitch} disabled={isDisabled} />
|
||||
<Typography
|
||||
fontSize="1rem"
|
||||
fontSize="0.875rem"
|
||||
color={value === true ? "auto" : "#bfbfbf"}
|
||||
onClick={(e) => setSwitch(e, true)}
|
||||
sx={{cursor: value === true || isDisabled ? "inherit" : "pointer"}}>
|
||||
Yes
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,6 @@ import React, {useState} from "react";
|
||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
interface Props
|
||||
@ -42,13 +41,16 @@ interface Props
|
||||
bulkEditMode?: boolean;
|
||||
bulkEditSwitchChangeHandler?: any;
|
||||
record?: QRecord;
|
||||
helpRoles?: string[];
|
||||
helpContentKeyPrefix?: string;
|
||||
}
|
||||
|
||||
function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler, record, helpRoles, helpContentKeyPrefix}: Props): JSX.Element
|
||||
function QDynamicForm(props: Props): JSX.Element
|
||||
{
|
||||
const {formFields, values, errors, touched} = formData;
|
||||
const {
|
||||
formData, formLabel, bulkEditMode, bulkEditSwitchChangeHandler,
|
||||
} = props;
|
||||
const {
|
||||
formFields, values, errors, touched,
|
||||
} = formData;
|
||||
|
||||
const formikProps = useFormikContext();
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
@ -68,8 +70,8 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
{
|
||||
setFileName(null);
|
||||
formikProps.setFieldValue(fieldName, null);
|
||||
record?.values.delete(fieldName)
|
||||
record?.displayValues.delete(fieldName)
|
||||
props.record?.values.delete(fieldName)
|
||||
props.record?.displayValues.delete(fieldName)
|
||||
};
|
||||
|
||||
const bulkEditSwitchChanged = (name: string, value: boolean) =>
|
||||
@ -77,7 +79,6 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
bulkEditSwitchChangeHandler(name, value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box lineHeight={0}>
|
||||
@ -95,38 +96,29 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
&& Object.keys(formFields).map((fieldName: any) =>
|
||||
{
|
||||
const field = formFields[fieldName];
|
||||
if (field.omitFromQDynamicForm)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (values[fieldName] === undefined)
|
||||
{
|
||||
values[fieldName] = "";
|
||||
}
|
||||
|
||||
let formattedHelpContent = <HelpContent helpContents={field.fieldMetaData.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
|
||||
if(formattedHelpContent)
|
||||
if (field.omitFromQDynamicForm)
|
||||
{
|
||||
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>
|
||||
return null;
|
||||
}
|
||||
|
||||
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
|
||||
<label htmlFor={field.name}>{field.label}</label>
|
||||
</Box>
|
||||
|
||||
if (field.type === "file")
|
||||
{
|
||||
const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB});
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
<Box mb={1.5}>
|
||||
{labelElement}
|
||||
|
||||
<InputLabel shrink={true}>{field.label}</InputLabel>
|
||||
{
|
||||
record && record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}>
|
||||
props.record && props.record.values.get(fieldName) && <Box fontSize="0.875rem" pb={1}>
|
||||
Current File:
|
||||
<Box display="inline-flex" pl={1}>
|
||||
{ValueUtils.getDisplayValue(pseudoField, record, "view")}
|
||||
{ValueUtils.getDisplayValue(pseudoField, props.record, "view")}
|
||||
<Tooltip placement="bottom" title="Remove current file">
|
||||
<Icon className="blobIcon" fontSize="small" onClick={(e) => removeFile(fieldName)}>delete</Icon>
|
||||
</Tooltip>
|
||||
@ -170,20 +162,18 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<DynamicSelect
|
||||
tableName={field.possibleValueProps.tableName}
|
||||
processName={field.possibleValueProps.processName}
|
||||
fieldName={fieldName}
|
||||
isEditable={field.isEditable}
|
||||
fieldLabel=""
|
||||
fieldLabel={field.label}
|
||||
initialValue={values[fieldName]}
|
||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@ -192,11 +182,9 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
// todo? placeholder={password.placeholder}
|
||||
return (
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<QDynamicFormField
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
label=""
|
||||
label={field.label}
|
||||
isEditable={field.isEditable}
|
||||
name={fieldName}
|
||||
displayFormat={field.displayFormat}
|
||||
@ -207,7 +195,6 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
success={`${values[fieldName]}` !== "" && !errors[fieldName] && touched[fieldName]}
|
||||
formFieldObject={field}
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
@ -220,7 +207,6 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
QDynamicForm.defaultProps = {
|
||||
formLabel: undefined,
|
||||
bulkEditMode: false,
|
||||
helpRoles: ["ALL_SCREENS"],
|
||||
bulkEditSwitchChangeHandler: () =>
|
||||
{
|
||||
},
|
||||
|
@ -25,7 +25,6 @@ import Switch from "@mui/material/Switch";
|
||||
import {ErrorMessage, Field, useFormikContext} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
@ -53,7 +52,6 @@ function QDynamicFormField({
|
||||
{
|
||||
const [switchChecked, setSwitchChecked] = useState(false);
|
||||
const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode);
|
||||
const {inputBorderColor} = colors;
|
||||
|
||||
const {setFieldValue} = useFormikContext();
|
||||
|
||||
@ -90,14 +88,7 @@ function QDynamicFormField({
|
||||
if (type === "checkbox")
|
||||
{
|
||||
getsBulkEditHtmlLabel = false;
|
||||
field = (<>
|
||||
<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />
|
||||
<Box mt={0.75}>
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</>);
|
||||
field = (<BooleanFieldSwitch name={name} label={label} value={value} isDisabled={isDisabled} />);
|
||||
}
|
||||
else if (type === "ace")
|
||||
{
|
||||
@ -124,7 +115,7 @@ function QDynamicFormField({
|
||||
width="100%"
|
||||
height="300px"
|
||||
value={value}
|
||||
style={{border: `1px solid ${inputBorderColor}`, borderRadius: "0.75rem"}}
|
||||
style={{border: "1px solid gray"}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -133,7 +124,7 @@ function QDynamicFormField({
|
||||
{
|
||||
field = (
|
||||
<>
|
||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="standard" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
onKeyPress={(e: any) =>
|
||||
{
|
||||
if (e.key === "Enter")
|
||||
@ -173,14 +164,6 @@ function QDynamicFormField({
|
||||
id={`bulkEditSwitch-${name}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
top: -3,
|
||||
position: "relative"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}>
|
||||
|
@ -89,7 +89,6 @@ class DynamicFormUtils
|
||||
label += field.isRequired ? " *" : "";
|
||||
|
||||
return ({
|
||||
fieldMetaData: field,
|
||||
name: field.name,
|
||||
label: label,
|
||||
isRequired: field.isRequired,
|
||||
|
@ -29,7 +29,6 @@ import Switch from "@mui/material/Switch";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {ErrorMessage, useFormikContext} from "formik";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
|
||||
@ -50,8 +49,6 @@ interface Props
|
||||
bulkEditMode?: boolean;
|
||||
bulkEditSwitchChangeHandler?: any;
|
||||
otherValues?: Map<string, any>;
|
||||
variant: "standard" | "outlined";
|
||||
initiallyOpen: boolean;
|
||||
}
|
||||
|
||||
DynamicSelect.defaultProps = {
|
||||
@ -66,8 +63,6 @@ DynamicSelect.defaultProps = {
|
||||
isMultiple: false,
|
||||
bulkEditMode: false,
|
||||
otherValues: new Map<string, any>(),
|
||||
variant: "outlined",
|
||||
initiallyOpen: false,
|
||||
bulkEditSwitchChangeHandler: () =>
|
||||
{
|
||||
},
|
||||
@ -75,14 +70,12 @@ DynamicSelect.defaultProps = {
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
||||
function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props)
|
||||
{
|
||||
const [open, setOpen] = useState(initiallyOpen);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState(null);
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
|
||||
const {inputBorderColor} = colors;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// default value - needs to be an array (from initialValues (array) prop) for multiple mode - //
|
||||
@ -116,14 +109,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
{
|
||||
// console.log("First render, so not searching...");
|
||||
setFirstRender(false);
|
||||
|
||||
/*
|
||||
if(!initiallyOpen)
|
||||
{
|
||||
console.log("returning because not initially open?");
|
||||
return;
|
||||
}
|
||||
*/
|
||||
return;
|
||||
}
|
||||
// console.log("Use effect for searchTerm - searching!");
|
||||
|
||||
@ -156,24 +142,6 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
};
|
||||
}, [ searchTerm ]);
|
||||
|
||||
// todo - finish... call it in onOpen?
|
||||
const reloadIfOtherValuesAreChanged = () =>
|
||||
{
|
||||
if(JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
setLoading(true);
|
||||
setOptions([]);
|
||||
console.log("Refreshing possible values...");
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName, searchTerm ?? "", null, otherValues);
|
||||
setLoading(false);
|
||||
setOptions([ ...results ]);
|
||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||
{
|
||||
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||
@ -262,7 +230,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
// attributes. so, doing this, w/ key=id, seemed to fix it. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (
|
||||
<li {...props} key={option.id} style={{fontSize: "1rem"}}>
|
||||
<li {...props} key={option.id}>
|
||||
{content}
|
||||
</li>
|
||||
);
|
||||
@ -276,35 +244,13 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// for outlined style, adjust some styles //
|
||||
////////////////////////////////////////////
|
||||
let autocompleteSX = {};
|
||||
if (variant == "outlined")
|
||||
{
|
||||
autocompleteSX = {
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "0.75rem",
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
padding: "0.5rem",
|
||||
background: isDisabled ? "#f0f2f5!important" : "initial",
|
||||
},
|
||||
"& .MuiOutlinedInput-root .MuiAutocomplete-input": {
|
||||
padding: "0",
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"& .Mui-disabled .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: inputBorderColor
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log(`default value: ${JSON.stringify(defaultValue)}`);
|
||||
|
||||
const autocomplete = (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
id={overrideId ?? fieldName}
|
||||
sx={autocompleteSX}
|
||||
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
|
||||
open={open}
|
||||
fullWidth
|
||||
onOpen={() =>
|
||||
@ -321,14 +267,9 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
{
|
||||
setOpen(false);
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option) =>
|
||||
{
|
||||
if(option === null || option === undefined)
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if(option && option.length)
|
||||
{
|
||||
@ -364,7 +305,7 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
<TextField
|
||||
{...params}
|
||||
label={fieldLabel}
|
||||
variant={variant}
|
||||
variant="standard"
|
||||
autoComplete="off"
|
||||
type="search"
|
||||
InputProps={{
|
||||
@ -400,14 +341,6 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
|
||||
id={`bulkEditSwitch-${fieldName}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
top: -3,
|
||||
position: "relative"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
|
@ -37,12 +37,10 @@ import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import * as Yup from "yup";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import QDynamicForm from "qqq/components/forms/DynamicForm";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import HelpContent from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
@ -81,7 +79,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
const [validations, setValidations] = useState({});
|
||||
const [initialValues, setInitialValues] = useState({} as { [key: string]: any });
|
||||
const [formFields, setFormFields] = useState(null as Map<string, any>);
|
||||
const [t1section, setT1Section] = useState(null as QTableSection);
|
||||
const [t1sectionName, setT1SectionName] = useState(null as string);
|
||||
const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
|
||||
|
||||
@ -154,9 +151,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||
return <QDynamicForm formData={formData} record={record} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableName};`} />;
|
||||
return <QDynamicForm formData={formData} record={record} />;
|
||||
}
|
||||
|
||||
if (!asyncLoadInited)
|
||||
@ -335,7 +330,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
/////////////////////////////////////
|
||||
const dynamicFormFieldsBySection = new Map<string, any>();
|
||||
let t1sectionName;
|
||||
let t1section;
|
||||
const nonT1Sections: QTableSection[] = [];
|
||||
for (let i = 0; i < tableSections.length; i++)
|
||||
{
|
||||
@ -388,7 +382,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
if (section.tier === "T1")
|
||||
{
|
||||
t1sectionName = section.name;
|
||||
t1section = section;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -396,7 +389,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
setT1SectionName(t1sectionName);
|
||||
setT1Section(t1section);
|
||||
setNonT1Sections(nonT1Sections);
|
||||
setFormFields(dynamicFormFieldsBySection);
|
||||
setValidations(Yup.object().shape(formValidations));
|
||||
@ -434,11 +426,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
actions.setSubmitting(true);
|
||||
await (async () =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we will be manipulating the values sent to the backend, so clone values so they remained unchanged for the form widgets //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const valuesToPost = JSON.parse(JSON.stringify(values));
|
||||
|
||||
for(let fieldName of tableMetaData.fields.keys())
|
||||
{
|
||||
const fieldMetaData = tableMetaData.fields.get(fieldName);
|
||||
@ -451,17 +438,17 @@ function EntityForm(props: Props): JSX.Element
|
||||
// changing from, say, 12:15:30 to just 12:15:00... this seems to get around that, for cases when the //
|
||||
// user didn't change the value in the field (but if the user did change the value, then we will submit it) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && valuesToPost[fieldName])
|
||||
if(fieldMetaData.type === QFieldType.DATE_TIME && values[fieldName])
|
||||
{
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${valuesToPost[fieldName]}]`)
|
||||
if (initialValues[fieldName] == valuesToPost[fieldName])
|
||||
console.log(`DateTime ${fieldName}: Initial value: [${initialValues[fieldName]}] -> [${values[fieldName]}]`)
|
||||
if (initialValues[fieldName] == values[fieldName])
|
||||
{
|
||||
console.log(" - Is the same, so, deleting from the post");
|
||||
delete (valuesToPost[fieldName]);
|
||||
delete (values[fieldName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
valuesToPost[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(valuesToPost[fieldName]);
|
||||
values[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(values[fieldName]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,14 +461,10 @@ function EntityForm(props: Props): JSX.Element
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldMetaData.type === QFieldType.BLOB)
|
||||
{
|
||||
if(typeof valuesToPost[fieldName] === "string")
|
||||
if(typeof values[fieldName] === "string")
|
||||
{
|
||||
console.log(`${fieldName} value was a string, so, we're deleting it from the values array, to not submit it to the backend, to not change it.`);
|
||||
delete(valuesToPost[fieldName]);
|
||||
}
|
||||
else
|
||||
{
|
||||
valuesToPost[fieldName] = values[fieldName];
|
||||
delete(values[fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -490,7 +473,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
{
|
||||
// todo - audit that it's a dupe
|
||||
await qController
|
||||
.update(tableName, props.id, valuesToPost)
|
||||
.update(tableName, props.id, values)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
@ -523,7 +506,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
else
|
||||
{
|
||||
await qController
|
||||
.create(tableName, valuesToPost)
|
||||
.create(tableName, values)
|
||||
.then((record) =>
|
||||
{
|
||||
if (props.isModal)
|
||||
@ -560,19 +543,6 @@ function EntityForm(props: Props): JSX.Element
|
||||
const formId = props.id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
|
||||
|
||||
let body;
|
||||
|
||||
const getSectionHelp = (section: QTableSection) =>
|
||||
{
|
||||
const helpRoles = [props.id ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||
const formattedHelpContent = <HelpContent helpContents={section.helpContents} roles={helpRoles} helpContentKey={`table:${tableMetaData.name};section:${section.name}`} />;
|
||||
|
||||
return formattedHelpContent && (
|
||||
<Box px={"1.5rem"} fontSize={"0.875rem"}>
|
||||
{formattedHelpContent}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (notAllowedError)
|
||||
{
|
||||
body = (
|
||||
@ -594,26 +564,23 @@ function EntityForm(props: Props): JSX.Element
|
||||
}
|
||||
else
|
||||
{
|
||||
const cardElevation = props.isModal ? 3 : 0;
|
||||
const cardElevation = props.isModal ? 3 : 1;
|
||||
body = (
|
||||
<Box mb={3}>
|
||||
{
|
||||
(alertContent || warningContent) &&
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
{alertContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{warningContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
{alertContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="error" onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{warningContent ? (
|
||||
<Box mb={3}>
|
||||
<Alert severity="warning" onClose={() => setWarningContent(null)}>{warningContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
</Grid>
|
||||
}
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
!props.isModal &&
|
||||
@ -651,11 +618,10 @@ function EntityForm(props: Props): JSX.Element
|
||||
<MDTypography variant="h5">{formTitle}</MDTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
{t1section && getSectionHelp(t1section)}
|
||||
{
|
||||
t1sectionName && formFields ? (
|
||||
<Box px={3}>
|
||||
<Box pb={"0.25rem"} width="100%">
|
||||
<Box pb={1} px={3}>
|
||||
<Box p={3} width="100%">
|
||||
{getFormSection(values, touched, formFields.get(t1sectionName), errors)}
|
||||
</Box>
|
||||
</Box>
|
||||
@ -669,9 +635,8 @@ function EntityForm(props: Props): JSX.Element
|
||||
<MDTypography variant="h6" p={3} pb={1}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
{getSectionHelp(section)}
|
||||
<Box pb={1} px={3}>
|
||||
<Box pb={"0.75rem"} width="100%">
|
||||
<Box p={3} width="100%">
|
||||
{getFormSection(values, touched, formFields.get(section.name), errors)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -25,7 +25,6 @@ import Icon from "@mui/material/Icon";
|
||||
import {ReactNode, useContext} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
|
||||
interface Props
|
||||
@ -70,7 +69,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
||||
}
|
||||
|
||||
const routes: string[] | any = route.slice(0, -1);
|
||||
const {pathToLabelMap, branding} = useContext(QContext);
|
||||
const {pageHeader, pathToLabelMap, branding} = useContext(QContext);
|
||||
|
||||
const fullPathToLabel = (fullPath: string, route: string): string =>
|
||||
{
|
||||
@ -92,18 +91,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
||||
let accumulatedPath = "";
|
||||
for (let i = 0; i < routes.length; i++)
|
||||
{
|
||||
////////////////////////////////////////////////////////
|
||||
// avoid showing "saved view" as a breadcrumb element //
|
||||
////////////////////////////////////////////////////////
|
||||
if(routes[i] === "savedView")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// avoid showing the table name if it's the element before savedView //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(i < routes.length - 1 && routes[i+1] == "savedView")
|
||||
if(routes[i] === "savedFilter")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -124,31 +112,48 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
||||
<Box mr={{xs: 0, xl: 8}}>
|
||||
<MuiBreadcrumbs
|
||||
sx={{
|
||||
fontSize: "1.125rem",
|
||||
fontWeight: "500",
|
||||
color: colors.dark.main,
|
||||
"& li": {
|
||||
lineHeight: "unset!important"
|
||||
},
|
||||
"& a": {
|
||||
color: colors.gray.main
|
||||
},
|
||||
"& .MuiBreadcrumbs-separator": {
|
||||
fontSize: "1.125rem",
|
||||
fontWeight: "500",
|
||||
color: colors.dark.main
|
||||
color: ({palette: {white, grey}}) => (light ? white.main : grey[600]),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Link to="/">
|
||||
<Icon sx={{fontSize: "1.25rem!important", position: "relative", top: "0.25rem"}}>{icon}</Icon>
|
||||
<MDTypography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color={light ? "white" : "dark"}
|
||||
opacity={light ? 0.8 : 0.5}
|
||||
sx={{lineHeight: 0}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</MDTypography>
|
||||
</Link>
|
||||
{fullRoutes.map((fullRoute: string) => (
|
||||
<Link to={fullRoute} key={fullRoute}>
|
||||
{fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
|
||||
<MDTypography
|
||||
component="span"
|
||||
variant="button"
|
||||
fontWeight="regular"
|
||||
textTransform="capitalize"
|
||||
color={light ? "white" : "dark"}
|
||||
opacity={light ? 0.8 : 0.5}
|
||||
sx={{lineHeight: 0}}
|
||||
>
|
||||
{fullPathToLabel(fullRoute, fullRoute.replace(/.*\//, ""))}
|
||||
</MDTypography>
|
||||
</Link>
|
||||
))}
|
||||
</MuiBreadcrumbs>
|
||||
<MDTypography
|
||||
pt={1}
|
||||
fontWeight="bold"
|
||||
textTransform="capitalize"
|
||||
variant="h5"
|
||||
color={light ? "white" : "dark"}
|
||||
noWrap
|
||||
>
|
||||
{pageHeader}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -19,22 +19,23 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Popper, InputAdornment} from "@mui/material";
|
||||
import {Popper} from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Badge from "@mui/material/Badge";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
||||
import {navbar, navbarContainer, navbarRow, navbarMobileMenu, recentlyViewedMenu,} from "qqq/components/horseshoe/Styles";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {setTransparentNavbar, useMaterialUIController, setMiniSidenav} from "qqq/context";
|
||||
import {navbar, navbarContainer, navbarIconButton, navbarRow,} from "qqq/components/horseshoe/Styles";
|
||||
import {setTransparentNavbar, useMaterialUIController,} from "qqq/context";
|
||||
import HistoryUtils from "qqq/utils/HistoryUtils";
|
||||
|
||||
// Declaring prop types for NavBar
|
||||
@ -56,7 +57,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
{
|
||||
const [navbarType, setNavbarType] = useState<"fixed" | "absolute" | "relative" | "static" | "sticky">();
|
||||
const [controller, dispatch] = useMaterialUIController();
|
||||
const {miniSidenav, transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const {transparentNavbar, fixedNavbar, darkMode,} = controller;
|
||||
const [openMenu, setOpenMenu] = useState<any>(false);
|
||||
const [history, setHistory] = useState<any>([] as HistoryEntry[]);
|
||||
const [autocompleteValue, setAutocompleteValue] = useState<any>(null);
|
||||
@ -64,8 +65,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
const route = useLocation().pathname.split("/").slice(1);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {pageHeader} = useContext(QContext);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
// Setting the navbar type
|
||||
@ -106,8 +105,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
return () => window.removeEventListener("scroll", handleTransparentNavbar);
|
||||
}, [dispatch, fixedNavbar]);
|
||||
|
||||
const handleMiniSidenav = () => setMiniSidenav(dispatch, !miniSidenav);
|
||||
|
||||
const goToHistory = (path: string) =>
|
||||
{
|
||||
navigate(path);
|
||||
@ -160,20 +157,12 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
options={history}
|
||||
autoHighlight
|
||||
blurOnSelect
|
||||
style={{width: "16rem"}}
|
||||
style={{width: "200px"}}
|
||||
onOpen={handleHistoryOnOpen}
|
||||
onChange={handleAutocompleteOnChange}
|
||||
PopperComponent={CustomPopper}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
sx={recentlyViewedMenu}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Icon sx={{position: "relative", right: "-1rem"}}>keyboard_arrow_down</Icon>
|
||||
</InputAdornment>
|
||||
)
|
||||
}} />}
|
||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" />}
|
||||
renderOption={(props, option: HistoryEntry) => (
|
||||
<Box {...props} component="li" key={option.id} sx={{width: "auto"}}>
|
||||
<Box sx={{width: "auto", px: "8px", whiteSpace: "overflow"}} key={option.id}>
|
||||
@ -186,6 +175,22 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Render the notifications menu
|
||||
const renderMenu = () => (
|
||||
<Menu
|
||||
anchorEl={openMenu}
|
||||
anchorReference={null}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={Boolean(openMenu)}
|
||||
onClose={handleCloseMenu}
|
||||
sx={{mt: 2}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Styles for the navbar icons
|
||||
const iconsStyle = ({
|
||||
palette: {dark, white, text},
|
||||
@ -235,27 +240,29 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
>
|
||||
<Toolbar sx={navbarContainer}>
|
||||
<Box color="inherit" mb={{xs: 1, md: 0}} sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<IconButton size="small" disableRipple color="inherit" sx={navbarMobileMenu} onClick={handleMiniSidenav}>
|
||||
<Icon sx={iconsStyle} fontSize="large">menu</Icon>
|
||||
</IconButton>
|
||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||
</Box>
|
||||
{isMini ? null : (
|
||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<Box pr={0} mr={-2}>
|
||||
<Box pr={1}>
|
||||
{renderHistory()}
|
||||
</Box>
|
||||
<Box color={light ? "white" : "inherit"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={navbarIconButton}
|
||||
onClick={handleOpenMenu}
|
||||
>
|
||||
<Badge badgeContent={0} color="error" variant="dot">
|
||||
<Icon sx={iconsStyle}>notifications</Icon>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
{renderMenu()}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
{
|
||||
pageHeader &&
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||
{pageHeader}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
}
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
function navbar(theme: Theme | any, ownerState: any)
|
||||
{
|
||||
@ -66,12 +65,12 @@ function navbar(theme: Theme | any, ownerState: any)
|
||||
return color;
|
||||
},
|
||||
top: absolute ? 0 : pxToRem(12),
|
||||
minHeight: "auto",
|
||||
minHeight: pxToRem(75),
|
||||
display: "grid",
|
||||
alignItems: "center",
|
||||
borderRadius: borderRadius.xl,
|
||||
paddingTop: pxToRem(0),
|
||||
paddingBottom: pxToRem(0),
|
||||
paddingTop: pxToRem(8),
|
||||
paddingBottom: pxToRem(8),
|
||||
paddingRight: absolute ? pxToRem(8) : 0,
|
||||
paddingLeft: absolute ? pxToRem(16) : 0,
|
||||
|
||||
@ -85,7 +84,7 @@ function navbar(theme: Theme | any, ownerState: any)
|
||||
"& .MuiToolbar-root": {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
alignItems: "center",
|
||||
|
||||
[breakpoints.up("sm")]: {
|
||||
minHeight: "auto",
|
||||
@ -99,10 +98,10 @@ const navbarContainer = ({breakpoints}: Theme): any => ({
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 !important",
|
||||
|
||||
[breakpoints.up("md")]: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingTop: "0",
|
||||
paddingBottom: "0",
|
||||
},
|
||||
@ -111,10 +110,11 @@ const navbarContainer = ({breakpoints}: Theme): any => ({
|
||||
const navbarRow = ({breakpoints}: Theme, {isMini}: any) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
|
||||
[breakpoints.up("md")]: {
|
||||
justifyContent: "stretch",
|
||||
justifyContent: isMini ? "space-between" : "stretch",
|
||||
width: isMini ? "100%" : "max-content",
|
||||
},
|
||||
|
||||
@ -146,38 +146,12 @@ const navbarDesktopMenu = ({breakpoints}: Theme) => ({
|
||||
display: "none !important",
|
||||
cursor: "pointer",
|
||||
|
||||
[breakpoints.down("sm")]: {
|
||||
[breakpoints.up("xl")]: {
|
||||
display: "inline-block !important",
|
||||
},
|
||||
});
|
||||
|
||||
const recentlyViewedMenu = ({breakpoints}: Theme) => ({
|
||||
marginTop: "-0.5rem",
|
||||
"& .MuiInputLabel-root": {
|
||||
color: colors.gray.main,
|
||||
fontWeight: "500",
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"& .MuiInputAdornment-root": {
|
||||
marginTop: "0.5rem",
|
||||
color: colors.gray.main,
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "0",
|
||||
padding: "0"
|
||||
},
|
||||
"& .MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline": {
|
||||
border: "0"
|
||||
},
|
||||
display: "block",
|
||||
[breakpoints.down("md")]: {
|
||||
display: "none !important",
|
||||
},
|
||||
});
|
||||
|
||||
const navbarMobileMenu = ({breakpoints}: Theme) => ({
|
||||
left: "-0.75rem",
|
||||
display: "inline-block",
|
||||
lineHeight: 0,
|
||||
|
||||
@ -193,5 +167,4 @@ export {
|
||||
navbarIconButton,
|
||||
navbarDesktopMenu,
|
||||
navbarMobileMenu,
|
||||
recentlyViewedMenu
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
const {palette, boxShadows, transitions, breakpoints, functions} = theme;
|
||||
const {transparentSidenav, whiteSidenav, miniSidenav, darkMode} = ownerState;
|
||||
|
||||
const sidebarWidth = 245;
|
||||
const sidebarWidth = 250;
|
||||
const {transparent, gradients, white, background} = palette;
|
||||
const {xxl} = boxShadows;
|
||||
const {pxToRem, linearGradient} = functions;
|
||||
@ -94,9 +94,6 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
||||
"& .MuiDrawer-paper": {
|
||||
boxShadow: xxl,
|
||||
border: "none",
|
||||
margin: "0",
|
||||
borderRadius: "0",
|
||||
height: "100%",
|
||||
|
||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||
},
|
||||
|
@ -64,8 +64,7 @@ function collapseItem(theme: Theme, ownerState: any)
|
||||
borderRadius: borderRadius.md,
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
whiteSpace: "wrap",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
boxShadow: active && !whiteSidenav && !darkMode && !transparentSidenav ? md : "none",
|
||||
[breakpoints.up("xl")]: {
|
||||
transition: transitions.create(["box-shadow", "background-color"], {
|
||||
@ -74,10 +73,6 @@ function collapseItem(theme: Theme, ownerState: any)
|
||||
}),
|
||||
},
|
||||
|
||||
"& .MuiListItemText-primary": {
|
||||
lineHeight: "revert"
|
||||
},
|
||||
|
||||
"&:hover, &:focus": {
|
||||
backgroundColor:
|
||||
transparentSidenav && !darkMode
|
||||
|
@ -69,15 +69,7 @@ export default styled(TextField)(({theme, ownerState}: { theme?: Theme; ownerSta
|
||||
});
|
||||
|
||||
return {
|
||||
"& .MuiInputBase-root": {
|
||||
backgroundColor: disabled ? `${grey[200]} !important` : transparent.main,
|
||||
borderRadius: "0.75rem",
|
||||
},
|
||||
"& input": {
|
||||
backgroundColor: `${transparent.main}!important`,
|
||||
padding: "0.5rem",
|
||||
fontSize: "1rem",
|
||||
},
|
||||
backgroundColor: disabled ? `${grey[200]} !important` : transparent.main,
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
...(error && errorStyles()),
|
||||
...(success && successStyles()),
|
||||
|
@ -149,7 +149,7 @@ interface Types {
|
||||
}
|
||||
|
||||
const baseProperties = {
|
||||
fontFamily: '"SF Pro Display", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
fontWeightLighter: 100,
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
|
@ -153,7 +153,7 @@ interface Types
|
||||
}
|
||||
|
||||
const baseProperties = {
|
||||
fontFamily: "\"SF Pro Display\", \"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
||||
fontFamily: "\"Roboto\", \"Helvetica\", \"Arial\", sans-serif",
|
||||
fontWeightLighter: 100,
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
|
||||
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 Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {ReactNode} 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[];
|
||||
}
|
||||
|
||||
FieldAutoComplete.defaultProps =
|
||||
{
|
||||
defaultValue: null,
|
||||
autoFocus: false,
|
||||
forceOpen: null,
|
||||
hiddenFieldNames: []
|
||||
};
|
||||
|
||||
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean, hiddenFieldNames: string[])
|
||||
{
|
||||
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
for (let i = 0; i < sortedFields.length; i++)
|
||||
{
|
||||
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
|
||||
|
||||
if(hiddenFieldNames && hiddenFieldNames.indexOf(fieldName) > -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
|
||||
}
|
||||
}
|
||||
|
||||
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames}: FieldAutoCompleteProps): JSX.Element
|
||||
{
|
||||
const fieldOptions: any[] = [];
|
||||
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames);
|
||||
let fieldsGroupBy = null;
|
||||
|
||||
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const exposedJoin = tableMetaData.exposedJoins[i];
|
||||
if (metaData.tables.has(exposedJoin.joinTable.name))
|
||||
{
|
||||
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
|
||||
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true, hiddenFieldNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getFieldOptionLabel(option: any)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - we're using renderFieldOption below for the actual select-box options, which //
|
||||
// are always jut field label (as they are under groupings that show their table name) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (option && option.field && option.table)
|
||||
{
|
||||
if (option.table.name == tableMetaData.name)
|
||||
{
|
||||
return (option.field.label);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (option.table.label + ": " + option.field.label);
|
||||
}
|
||||
}
|
||||
|
||||
return ("");
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for options, we only want the field label (contrast with what we show in the input box, //
|
||||
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
|
||||
{
|
||||
let label = "";
|
||||
if (option && option.field)
|
||||
{
|
||||
label = (option.field.label);
|
||||
}
|
||||
|
||||
return (<li {...props}>{label}</li>);
|
||||
}
|
||||
|
||||
|
||||
function isFieldOptionEqual(option: any, value: any)
|
||||
{
|
||||
return option.fieldName === value.fieldName;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// seems like, if we always add the open attribute, then if its false or null, then the autocomplete //
|
||||
// doesn't open at all... so, only add the attribute at all, if forceOpen is true //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const alsoOpen: {[key: string]: any} = {}
|
||||
if(forceOpen)
|
||||
{
|
||||
alsoOpen["open"] = forceOpen;
|
||||
}
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
id={id}
|
||||
renderInput={(params) => (<TextField {...params} autoFocus={autoFocus} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
// @ts-ignore
|
||||
defaultValue={defaultValue}
|
||||
options={fieldOptions}
|
||||
onChange={handleFieldChange}
|
||||
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
|
||||
groupBy={fieldsGroupBy}
|
||||
getOptionLabel={(option) => getFieldOptionLabel(option)}
|
||||
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
|
||||
autoSelect={true}
|
||||
autoHighlight={true}
|
||||
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
||||
{...alsoOpen}
|
||||
/>
|
||||
|
||||
);
|
||||
}
|
@ -34,6 +34,7 @@ import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
@ -195,8 +196,17 @@ function GotoRecordDialog(props: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
|
||||
<DialogTitle>Go To...</DialogTitle>
|
||||
|
||||
<DialogTitle sx={{display: "flex"}}>
|
||||
<Box sx={{display: "flex", flexGrow: 1}}>
|
||||
Go To...
|
||||
</Box>
|
||||
<Box sx={{display: "flex"}}>
|
||||
<IconButton onClick={() =>
|
||||
{
|
||||
document.location.href = "/";
|
||||
}}><Icon sx={{align: "right"}} fontSize="small">close</Icon></IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{props.subHeader}
|
||||
{
|
||||
|
@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
|
||||
import Box from "@mui/material/Box";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useContext} from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import QContext from "QContext";
|
||||
|
||||
interface Props
|
||||
{
|
||||
helpContents: QHelpContent[];
|
||||
roles: string[];
|
||||
heading?: string;
|
||||
helpContentKey?: string;
|
||||
}
|
||||
|
||||
HelpContent.defaultProps = {};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** format some content - meaning, change it from string to JSX element(s) or string.
|
||||
** does a parse() for HTML, and a <Markdown> for markdown, else just text.
|
||||
*******************************************************************************/
|
||||
const formatHelpContent = (content: string, format: string): string | JSX.Element | JSX.Element[] =>
|
||||
{
|
||||
if (format == "HTML")
|
||||
{
|
||||
return parse(content);
|
||||
}
|
||||
else if (format == "MARKDOWN")
|
||||
{
|
||||
return (<Markdown>{content}</Markdown>)
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** return the first help content from the list that matches the first role
|
||||
** in the roles list.
|
||||
*******************************************************************************/
|
||||
const getMatchingHelpContent = (helpContents: QHelpContent[], roles: string[]): QHelpContent =>
|
||||
{
|
||||
if (helpContents)
|
||||
{
|
||||
if (helpContents.length == 1 && helpContents[0].roles.size == 0)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's only 1 entry, and it has no roles, then assume user wanted it globally and use it //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (helpContents[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (let i = 0; i < roles.length; i++)
|
||||
{
|
||||
for (let j = 0; j < helpContents.length; j++)
|
||||
{
|
||||
if (helpContents[j].roles.has(roles[i]))
|
||||
{
|
||||
return(helpContents[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** test if a list of help contents would find any matches from a list of roles.
|
||||
*******************************************************************************/
|
||||
export const hasHelpContent = (helpContents: QHelpContent[], roles: string[]) =>
|
||||
{
|
||||
return getMatchingHelpContent(helpContents, roles) != null;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** component that renders a box of formatted help content, from a list of
|
||||
** helpContents (from meta-data), and for a list of roles (based on what screen
|
||||
*******************************************************************************/
|
||||
function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX.Element
|
||||
{
|
||||
const {helpHelpActive} = useContext(QContext);
|
||||
let selectedHelpContent = getMatchingHelpContent(helpContents, roles);
|
||||
|
||||
let content = null;
|
||||
if (helpHelpActive)
|
||||
{
|
||||
if (!selectedHelpContent)
|
||||
{
|
||||
selectedHelpContent = new QHelpContent({content: ""});
|
||||
}
|
||||
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
|
||||
}
|
||||
else if(selectedHelpContent)
|
||||
{
|
||||
content = selectedHelpContent.content;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// if content was found, format it and return it //
|
||||
///////////////////////////////////////////////////
|
||||
if (content)
|
||||
{
|
||||
return <Box display="inline" className="helpContent">
|
||||
{heading && <span className="header">{heading}</span>}
|
||||
{formatHelpContent(content, selectedHelpContent.format)}
|
||||
</Box>;
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
export default HelpContent;
|
@ -41,7 +41,7 @@ interface Props
|
||||
|
||||
QRecordSidebar.defaultProps = {
|
||||
light: false,
|
||||
stickyTop: "1rem",
|
||||
stickyTop: "110px",
|
||||
};
|
||||
|
||||
interface SidebarEntry
|
||||
@ -76,7 +76,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
|
||||
|
||||
|
||||
return (
|
||||
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "auto", maxHeight: "calc(100vh - 2rem)"}}>
|
||||
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: stickyTop}}>
|
||||
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
||||
{
|
||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
||||
|
511
src/qqq/components/misc/SavedFilters.tsx
Normal file
511
src/qqq/components/misc/SavedFilters.tsx
Normal file
@ -0,0 +1,511 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {FiberManualRecord} from "@mui/icons-material";
|
||||
import {Alert} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Divider from "@mui/material/Divider";
|
||||
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 TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {GridFilterModel, GridSortItem} from "@mui/x-data-grid-pro";
|
||||
import FormData from "form-data";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import {QCancelButton, QDeleteButton, QSaveButton, QSavedFiltersMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
qController: QController;
|
||||
metaData: QInstance;
|
||||
tableMetaData: QTableMetaData;
|
||||
currentSavedFilter: QRecord;
|
||||
filterModel?: GridFilterModel;
|
||||
columnSortModel?: GridSortItem[];
|
||||
filterOnChangeCallback?: (selectedSavedFilterId: number) => void;
|
||||
}
|
||||
|
||||
function SavedFilters({qController, metaData, tableMetaData, currentSavedFilter, filterModel, columnSortModel, filterOnChangeCallback}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [savedFilters, setSavedFilters] = useState([] as QRecord[]);
|
||||
const [savedFiltersMenu, setSavedFiltersMenu] = useState(null);
|
||||
const [savedFiltersHaveLoaded, setSavedFiltersHaveLoaded] = useState(false);
|
||||
const [filterIsModified, setFilterIsModified] = useState(false);
|
||||
|
||||
const [saveFilterPopupOpen, setSaveFilterPopupOpen] = useState(false);
|
||||
const [isSaveFilterAs, setIsSaveFilterAs] = useState(false);
|
||||
const [isRenameFilter, setIsRenameFilter] = useState(false);
|
||||
const [isDeleteFilter, setIsDeleteFilter] = useState(false);
|
||||
const [savedFilterNameInputValue, setSavedFilterNameInputValue] = useState(null as string);
|
||||
const [popupAlertContent, setPopupAlertContent] = useState("");
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const location = useLocation();
|
||||
const [saveOptionsOpen, setSaveOptionsOpen] = useState(false);
|
||||
|
||||
const SAVE_OPTION = "Save...";
|
||||
const DUPLICATE_OPTION = "Duplicate...";
|
||||
const RENAME_OPTION = "Rename...";
|
||||
const DELETE_OPTION = "Delete...";
|
||||
const CLEAR_OPTION = "Clear Current Filter";
|
||||
const dropdownOptions = [DUPLICATE_OPTION, RENAME_OPTION, DELETE_OPTION, CLEAR_OPTION];
|
||||
|
||||
const openSavedFiltersMenu = (event: any) => setSavedFiltersMenu(event.currentTarget);
|
||||
const closeSavedFiltersMenu = () => setSavedFiltersMenu(null);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// load filters on first run, then monitor location or metadata changes //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
loadSavedFilters()
|
||||
.then(() =>
|
||||
{
|
||||
if (currentSavedFilter != null)
|
||||
{
|
||||
let qFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel);
|
||||
setFilterIsModified(JSON.stringify(qFilter) !== currentSavedFilter.values.get("filterJson"));
|
||||
}
|
||||
|
||||
setSavedFiltersHaveLoaded(true);
|
||||
});
|
||||
}, [location , tableMetaData, currentSavedFilter, filterModel, columnSortModel])
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** make request to load all saved filters from backend
|
||||
*******************************************************************************/
|
||||
async function loadSavedFilters()
|
||||
{
|
||||
if (! tableMetaData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
|
||||
let savedFilters = await makeSavedFilterRequest("querySavedFilter", formData);
|
||||
setSavedFilters(savedFilters);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a saved record is clicked from the dropdown
|
||||
*******************************************************************************/
|
||||
const handleSavedFilterRecordOnClick = async (record: QRecord) =>
|
||||
{
|
||||
setSaveFilterPopupOpen(false);
|
||||
closeSavedFiltersMenu();
|
||||
filterOnChangeCallback(record.values.get("id"));
|
||||
navigate(`${metaData.getTablePathByName(tableMetaData.name)}/savedFilter/${record.values.get("id")}`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a save option is selected from the save... button/dropdown combo
|
||||
*******************************************************************************/
|
||||
const handleDropdownOptionClick = (optionName: string) =>
|
||||
{
|
||||
setSaveOptionsOpen(false);
|
||||
setPopupAlertContent(null);
|
||||
closeSavedFiltersMenu();
|
||||
setSaveFilterPopupOpen(true);
|
||||
setIsSaveFilterAs(false);
|
||||
setIsRenameFilter(false);
|
||||
setIsDeleteFilter(false)
|
||||
|
||||
switch(optionName)
|
||||
{
|
||||
case SAVE_OPTION:
|
||||
break;
|
||||
case DUPLICATE_OPTION:
|
||||
setIsSaveFilterAs(true);
|
||||
break;
|
||||
case CLEAR_OPTION:
|
||||
setSaveFilterPopupOpen(false)
|
||||
filterOnChangeCallback(null);
|
||||
navigate(metaData.getTablePathByName(tableMetaData.name));
|
||||
break;
|
||||
case RENAME_OPTION:
|
||||
if(currentSavedFilter != null)
|
||||
{
|
||||
setSavedFilterNameInputValue(currentSavedFilter.values.get("label"));
|
||||
}
|
||||
setIsRenameFilter(true);
|
||||
break;
|
||||
case DELETE_OPTION:
|
||||
setIsDeleteFilter(true)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when save or delete button saved on confirmation dialogs
|
||||
*******************************************************************************/
|
||||
async function handleFilterDialogButtonOnClick()
|
||||
{
|
||||
try
|
||||
{
|
||||
const formData = new FormData();
|
||||
if (isDeleteFilter)
|
||||
{
|
||||
formData.append("id", currentSavedFilter.values.get("id"));
|
||||
await makeSavedFilterRequest("deleteSavedFilter", formData);
|
||||
await(async() =>
|
||||
{
|
||||
handleDropdownOptionClick(CLEAR_OPTION);
|
||||
})();
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
formData.append("filterJson", JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel))));
|
||||
|
||||
if (isSaveFilterAs || isRenameFilter || currentSavedFilter == null)
|
||||
{
|
||||
formData.append("label", savedFilterNameInputValue);
|
||||
if(currentSavedFilter != null && isRenameFilter)
|
||||
{
|
||||
formData.append("id", currentSavedFilter.values.get("id"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.append("id", currentSavedFilter.values.get("id"));
|
||||
formData.append("label", currentSavedFilter?.values.get("label"));
|
||||
}
|
||||
const recordList = await makeSavedFilterRequest("storeSavedFilter", formData);
|
||||
await(async() =>
|
||||
{
|
||||
if (recordList && recordList.length > 0)
|
||||
{
|
||||
setSavedFiltersHaveLoaded(false);
|
||||
loadSavedFilters();
|
||||
handleSavedFilterRecordOnClick(recordList[0]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
catch (e: any)
|
||||
{
|
||||
setPopupAlertContent(JSON.stringify(e.message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** hides/shows the save options
|
||||
*******************************************************************************/
|
||||
const handleToggleSaveOptions = () =>
|
||||
{
|
||||
setSaveOptionsOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes save options menu (on clickaway)
|
||||
*******************************************************************************/
|
||||
const handleSaveOptionsMenuClose = (event: Event) =>
|
||||
{
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveOptionsOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** stores the current dialog input text to state
|
||||
*******************************************************************************/
|
||||
const handleSaveFilterInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
|
||||
{
|
||||
setSavedFilterNameInputValue(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes current dialog
|
||||
*******************************************************************************/
|
||||
const handleSaveFilterPopupClose = () =>
|
||||
{
|
||||
setSaveFilterPopupOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** make a request to the backend for various savedFilter processes
|
||||
*******************************************************************************/
|
||||
async function makeSavedFilterRequest(processName: string, formData: FormData): Promise<QRecord[]>
|
||||
{
|
||||
/////////////////////////
|
||||
// fetch saved filters //
|
||||
/////////////////////////
|
||||
let savedFilters = [] as QRecord[]
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// we don't want this job to go async, so, pass a large timeout //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000);
|
||||
const processResult = await qController.processInit(processName, formData, qController.defaultMultipartFormDataHeaders());
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError;
|
||||
throw(jobError.error);
|
||||
}
|
||||
else
|
||||
{
|
||||
const result = processResult as QJobComplete;
|
||||
if(result.values.savedFilterList)
|
||||
{
|
||||
for (let i = 0; i < result.values.savedFilterList.length; i++)
|
||||
{
|
||||
const qRecord = new QRecord(result.values.savedFilterList[i]);
|
||||
savedFilters.push(qRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
throw(e);
|
||||
}
|
||||
|
||||
return (savedFilters);
|
||||
}
|
||||
|
||||
const hasStorePermission = metaData?.processes.has("storeSavedFilter");
|
||||
const hasDeletePermission = metaData?.processes.has("deleteSavedFilter");
|
||||
const hasQueryPermission = metaData?.processes.has("querySavedFilter");
|
||||
|
||||
const renderSavedFiltersMenu = tableMetaData && (
|
||||
<Menu
|
||||
anchorEl={savedFiltersMenu}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
open={Boolean(savedFiltersMenu)}
|
||||
onClose={closeSavedFiltersMenu}
|
||||
keepMounted
|
||||
>
|
||||
<MenuItem sx={{width: "300px"}} disabled style={{"opacity": "initial"}}><b>Filter Actions</b></MenuItem>
|
||||
{
|
||||
hasStorePermission &&
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
|
||||
<ListItemIcon><Icon>save</Icon></ListItemIcon>
|
||||
Save...
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
hasStorePermission &&
|
||||
<MenuItem disabled={currentSavedFilter === null} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
Rename...
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
hasStorePermission &&
|
||||
<MenuItem disabled={currentSavedFilter === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||
Duplicate...
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
hasDeletePermission &&
|
||||
<MenuItem disabled={currentSavedFilter === null} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
Delete...
|
||||
</MenuItem>
|
||||
}
|
||||
{
|
||||
<MenuItem disabled={currentSavedFilter === null} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||
<ListItemIcon><Icon>clear</Icon></ListItemIcon>
|
||||
Clear Current Filter
|
||||
</MenuItem>
|
||||
}
|
||||
<Divider/>
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Filters</b></MenuItem>
|
||||
{
|
||||
savedFilters && savedFilters.length > 0 ? (
|
||||
savedFilters.map((record: QRecord, index: number) =>
|
||||
<MenuItem sx={{paddingLeft: "50px"}} key={`savedFiler-${index}`} onClick={() => handleSavedFilterRecordOnClick(record)}>
|
||||
{record.values.get("label")}
|
||||
</MenuItem>
|
||||
)
|
||||
): (
|
||||
<MenuItem >
|
||||
<i>No filters have been saved for this table.</i>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
hasQueryPermission && tableMetaData ? (
|
||||
<Box display="flex" flexGrow={1}>
|
||||
<QSavedFiltersMenuButton isOpen={savedFiltersMenu} onClickHandler={openSavedFiltersMenu} />
|
||||
{renderSavedFiltersMenu}
|
||||
<Box display="flex" justifyContent="center" flexDirection="column">
|
||||
<Box pl={2} pr={2} sx={{display: "flex", alignItems: "center"}}>
|
||||
{
|
||||
savedFiltersHaveLoaded && currentSavedFilter && (
|
||||
<Typography mr={2} variant="h6">Current Filter:
|
||||
<span style={{fontWeight: "initial"}}>
|
||||
{currentSavedFilter.values.get("label")}
|
||||
{
|
||||
filterIsModified && (
|
||||
<Tooltip sx={{cursor: "pointer"}} title={"The current filter has been modified. Click \"Save...\" to save the changes."}>
|
||||
<FiberManualRecord sx={{color: "orange", paddingLeft: "2px", paddingTop: "4px"}} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{
|
||||
<Dialog
|
||||
open={saveFilterPopupOpen}
|
||||
onClose={handleSaveFilterPopupClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyPress={(e) =>
|
||||
{
|
||||
if (e.key == "Enter")
|
||||
{
|
||||
handleFilterDialogButtonOnClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{
|
||||
currentSavedFilter ? (
|
||||
isDeleteFilter ? (
|
||||
<DialogTitle id="alert-dialog-title">Delete Filter</DialogTitle>
|
||||
) : (
|
||||
isSaveFilterAs ? (
|
||||
<DialogTitle id="alert-dialog-title">Save Filter As</DialogTitle>
|
||||
):(
|
||||
isRenameFilter ? (
|
||||
<DialogTitle id="alert-dialog-title">Rename Filter</DialogTitle>
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Update Existing Filter</DialogTitle>
|
||||
)
|
||||
)
|
||||
)
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Save New Filter</DialogTitle>
|
||||
)
|
||||
}
|
||||
<DialogContent sx={{width: "500px"}}>
|
||||
{
|
||||
(! currentSavedFilter || isSaveFilterAs || isRenameFilter) && ! isDeleteFilter ? (
|
||||
<Box>
|
||||
{
|
||||
isSaveFilterAs ? (
|
||||
<Box mb={3}>Enter a name for this new saved filter.</Box>
|
||||
):(
|
||||
<Box mb={3}>Enter a new name for this saved filter.</Box>
|
||||
)
|
||||
}
|
||||
<TextField
|
||||
autoFocus
|
||||
name="custom-delimiter-value"
|
||||
placeholder="Filter Name"
|
||||
label="Filter Name"
|
||||
inputProps={{width: "100%", maxLength: 100}}
|
||||
value={savedFilterNameInputValue}
|
||||
sx={{width: "100%"}}
|
||||
onChange={handleSaveFilterInputChange}
|
||||
onFocus={event =>
|
||||
{
|
||||
event.target.select();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
):(
|
||||
isDeleteFilter ? (
|
||||
<Box>Are you sure you want to delete the filter {`'${currentSavedFilter?.values.get("label")}'`}?</Box>
|
||||
):(
|
||||
<Box>Are you sure you want to update the filter {`'${currentSavedFilter?.values.get("label")}'`} with the current filter criteria?</Box>
|
||||
)
|
||||
)
|
||||
}
|
||||
{popupAlertContent ? (
|
||||
<Box m={1}>
|
||||
<Alert severity="error">{popupAlertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<QCancelButton onClickHandler={handleSaveFilterPopupClose} disabled={false} />
|
||||
{
|
||||
isDeleteFilter ?
|
||||
<QDeleteButton onClickHandler={handleFilterDialogButtonOnClick} />
|
||||
:
|
||||
<QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={(isSaveFilterAs || currentSavedFilter == null) && savedFilterNameInputValue == null}/>
|
||||
}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
||||
</Box>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default SavedFilters;
|
@ -1,682 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Alert, Button, Link} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Divider from "@mui/material/Divider";
|
||||
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 TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {TooltipProps} from "@mui/material/Tooltip/Tooltip";
|
||||
import FormData from "form-data";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QDeleteButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import RecordQueryView from "qqq/models/query/RecordQueryView";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import {SavedViewUtils} from "qqq/utils/qqq/SavedViewUtils";
|
||||
|
||||
interface Props
|
||||
{
|
||||
qController: QController;
|
||||
metaData: QInstance;
|
||||
tableMetaData: QTableMetaData;
|
||||
currentSavedView: QRecord;
|
||||
tableDefaultView: RecordQueryView;
|
||||
view?: RecordQueryView;
|
||||
viewAsJson?: string;
|
||||
viewOnChangeCallback?: (selectedSavedViewId: number) => void;
|
||||
loadingSavedView: boolean
|
||||
}
|
||||
|
||||
function SavedViews({qController, metaData, tableMetaData, currentSavedView, tableDefaultView, view, viewAsJson, viewOnChangeCallback, loadingSavedView}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [savedViews, setSavedViews] = useState([] as QRecord[]);
|
||||
const [savedViewsMenu, setSavedViewsMenu] = useState(null);
|
||||
const [savedViewsHaveLoaded, setSavedViewsHaveLoaded] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [saveFilterPopupOpen, setSaveFilterPopupOpen] = useState(false);
|
||||
const [isSaveFilterAs, setIsSaveFilterAs] = useState(false);
|
||||
const [isRenameFilter, setIsRenameFilter] = useState(false);
|
||||
const [isDeleteFilter, setIsDeleteFilter] = useState(false);
|
||||
const [savedViewNameInputValue, setSavedViewNameInputValue] = useState(null as string);
|
||||
const [popupAlertContent, setPopupAlertContent] = useState("");
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const location = useLocation();
|
||||
const [saveOptionsOpen, setSaveOptionsOpen] = useState(false);
|
||||
|
||||
const SAVE_OPTION = "Save...";
|
||||
const DUPLICATE_OPTION = "Duplicate...";
|
||||
const RENAME_OPTION = "Rename...";
|
||||
const DELETE_OPTION = "Delete...";
|
||||
const CLEAR_OPTION = "New View";
|
||||
const dropdownOptions = [DUPLICATE_OPTION, RENAME_OPTION, DELETE_OPTION, CLEAR_OPTION];
|
||||
|
||||
const {accentColor, accentColorLight} = useContext(QContext);
|
||||
|
||||
const openSavedViewsMenu = (event: any) => setSavedViewsMenu(event.currentTarget);
|
||||
const closeSavedViewsMenu = () => setSavedViewsMenu(null);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// load filters on first run, then monitor location or metadata changes //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
loadSavedViews()
|
||||
.then(() =>
|
||||
{
|
||||
setSavedViewsHaveLoaded(true);
|
||||
});
|
||||
}, [location, tableMetaData])
|
||||
|
||||
|
||||
const baseView = currentSavedView ? JSON.parse(currentSavedView.values.get("viewJson")) as RecordQueryView : tableDefaultView;
|
||||
const viewDiffs = SavedViewUtils.diffViews(tableMetaData, baseView, view);
|
||||
let viewIsModified = false;
|
||||
if(viewDiffs.length > 0)
|
||||
{
|
||||
viewIsModified = true;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** make request to load all saved filters from backend
|
||||
*******************************************************************************/
|
||||
async function loadSavedViews()
|
||||
{
|
||||
if (! tableMetaData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
|
||||
let savedViews = await makeSavedViewRequest("querySavedView", formData);
|
||||
setSavedViews(savedViews);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a saved record is clicked from the dropdown
|
||||
*******************************************************************************/
|
||||
const handleSavedViewRecordOnClick = async (record: QRecord) =>
|
||||
{
|
||||
setSaveFilterPopupOpen(false);
|
||||
closeSavedViewsMenu();
|
||||
viewOnChangeCallback(record.values.get("id"));
|
||||
navigate(`${metaData.getTablePathByName(tableMetaData.name)}/savedView/${record.values.get("id")}`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when a save option is selected from the save... button/dropdown combo
|
||||
*******************************************************************************/
|
||||
const handleDropdownOptionClick = (optionName: string) =>
|
||||
{
|
||||
setSaveOptionsOpen(false);
|
||||
setPopupAlertContent("");
|
||||
closeSavedViewsMenu();
|
||||
setSaveFilterPopupOpen(true);
|
||||
setIsSaveFilterAs(false);
|
||||
setIsRenameFilter(false);
|
||||
setIsDeleteFilter(false)
|
||||
|
||||
switch(optionName)
|
||||
{
|
||||
case SAVE_OPTION:
|
||||
if(currentSavedView == null)
|
||||
{
|
||||
setSavedViewNameInputValue("");
|
||||
}
|
||||
break;
|
||||
case DUPLICATE_OPTION:
|
||||
setSavedViewNameInputValue("");
|
||||
setIsSaveFilterAs(true);
|
||||
break;
|
||||
case CLEAR_OPTION:
|
||||
setSaveFilterPopupOpen(false)
|
||||
viewOnChangeCallback(null);
|
||||
navigate(metaData.getTablePathByName(tableMetaData.name));
|
||||
break;
|
||||
case RENAME_OPTION:
|
||||
if(currentSavedView != null)
|
||||
{
|
||||
setSavedViewNameInputValue(currentSavedView.values.get("label"));
|
||||
}
|
||||
setIsRenameFilter(true);
|
||||
break;
|
||||
case DELETE_OPTION:
|
||||
setIsDeleteFilter(true)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fired when save or delete button saved on confirmation dialogs
|
||||
*******************************************************************************/
|
||||
async function handleFilterDialogButtonOnClick()
|
||||
{
|
||||
try
|
||||
{
|
||||
setPopupAlertContent("");
|
||||
setIsSubmitting(true);
|
||||
|
||||
const formData = new FormData();
|
||||
if (isDeleteFilter)
|
||||
{
|
||||
formData.append("id", currentSavedView.values.get("id"));
|
||||
await makeSavedViewRequest("deleteSavedView", formData);
|
||||
|
||||
setSaveFilterPopupOpen(false);
|
||||
setSaveOptionsOpen(false);
|
||||
|
||||
await(async() =>
|
||||
{
|
||||
handleDropdownOptionClick(CLEAR_OPTION);
|
||||
})();
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// clone view via json serialization/deserialization //
|
||||
// then replace the viewJson in it with a copy that has had its possible values changed to ids //
|
||||
// then stringify that for the backend //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const viewObject = JSON.parse(JSON.stringify(view));
|
||||
viewObject.queryFilter = JSON.parse(JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(viewObject.queryFilter)));
|
||||
formData.append("viewJson", JSON.stringify(viewObject));
|
||||
|
||||
if (isSaveFilterAs || isRenameFilter || currentSavedView == null)
|
||||
{
|
||||
formData.append("label", savedViewNameInputValue);
|
||||
if(currentSavedView != null && isRenameFilter)
|
||||
{
|
||||
formData.append("id", currentSavedView.values.get("id"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.append("id", currentSavedView.values.get("id"));
|
||||
formData.append("label", currentSavedView?.values.get("label"));
|
||||
}
|
||||
const recordList = await makeSavedViewRequest("storeSavedView", formData);
|
||||
await(async() =>
|
||||
{
|
||||
if (recordList && recordList.length > 0)
|
||||
{
|
||||
setSavedViewsHaveLoaded(false);
|
||||
loadSavedViews();
|
||||
handleSavedViewRecordOnClick(recordList[0]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
setSaveFilterPopupOpen(false);
|
||||
setSaveOptionsOpen(false);
|
||||
}
|
||||
catch (e: any)
|
||||
{
|
||||
let message = JSON.stringify(e);
|
||||
if(typeof e == "string")
|
||||
{
|
||||
message = e;
|
||||
}
|
||||
else if(typeof e == "object" && e.message)
|
||||
{
|
||||
message = e.message;
|
||||
}
|
||||
|
||||
setPopupAlertContent(message);
|
||||
console.log(`Setting error: ${message}`);
|
||||
}
|
||||
finally
|
||||
{
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** hides/shows the save options
|
||||
*******************************************************************************/
|
||||
const handleToggleSaveOptions = () =>
|
||||
{
|
||||
setSaveOptionsOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes save options menu (on clickaway)
|
||||
*******************************************************************************/
|
||||
const handleSaveOptionsMenuClose = (event: Event) =>
|
||||
{
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveOptionsOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** stores the current dialog input text to state
|
||||
*******************************************************************************/
|
||||
const handleSaveFilterInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
|
||||
{
|
||||
setSavedViewNameInputValue(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** closes current dialog
|
||||
*******************************************************************************/
|
||||
const handleSaveFilterPopupClose = () =>
|
||||
{
|
||||
setSaveFilterPopupOpen(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** make a request to the backend for various savedView processes
|
||||
*******************************************************************************/
|
||||
async function makeSavedViewRequest(processName: string, formData: FormData): Promise<QRecord[]>
|
||||
{
|
||||
/////////////////////////
|
||||
// fetch saved filters //
|
||||
/////////////////////////
|
||||
let savedViews = [] as QRecord[]
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// we don't want this job to go async, so, pass a large timeout //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000);
|
||||
const processResult = await qController.processInit(processName, formData, qController.defaultMultipartFormDataHeaders());
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError;
|
||||
throw(jobError.error);
|
||||
}
|
||||
else
|
||||
{
|
||||
const result = processResult as QJobComplete;
|
||||
if(result.values.savedViewList)
|
||||
{
|
||||
for (let i = 0; i < result.values.savedViewList.length; i++)
|
||||
{
|
||||
const qRecord = new QRecord(result.values.savedViewList[i]);
|
||||
savedViews.push(qRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
throw(e);
|
||||
}
|
||||
|
||||
return (savedViews);
|
||||
}
|
||||
|
||||
const hasStorePermission = metaData?.processes.has("storeSavedView");
|
||||
const hasDeletePermission = metaData?.processes.has("deleteSavedView");
|
||||
const hasQueryPermission = metaData?.processes.has("querySavedView");
|
||||
|
||||
const tooltipMaxWidth = (maxWidth: string) =>
|
||||
{
|
||||
return ({slotProps: {
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: maxWidth
|
||||
}
|
||||
}
|
||||
}})
|
||||
}
|
||||
|
||||
const menuTooltipAttribs = {...tooltipMaxWidth("250px"), placement: "left", enterDelay: 1000} as TooltipProps;
|
||||
|
||||
const renderSavedViewsMenu = tableMetaData && (
|
||||
<Menu
|
||||
anchorEl={savedViewsMenu}
|
||||
anchorOrigin={{vertical: "bottom", horizontal: "left",}}
|
||||
transformOrigin={{vertical: "top", horizontal: "left",}}
|
||||
open={Boolean(savedViewsMenu)}
|
||||
onClose={closeSavedViewsMenu}
|
||||
keepMounted
|
||||
PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minHeight: "200px"}}}
|
||||
>
|
||||
<MenuItem sx={{width: "300px"}} disabled style={{"opacity": "initial"}}><b>View Actions</b></MenuItem>
|
||||
{
|
||||
hasStorePermission &&
|
||||
<Tooltip {...menuTooltipAttribs} title={<>Save your current filters, columns and settings, for quick re-use at a later time.<br /><br />You will be prompted to enter a name if you choose this option.</>}>
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>
|
||||
<ListItemIcon><Icon>save</Icon></ListItemIcon>
|
||||
{currentSavedView ? "Save..." : "Save As..."}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
hasStorePermission && currentSavedView != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Change the name for this saved view.">
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||
Rename...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
hasStorePermission && currentSavedView != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Save a new copy this view, with a different name, separate from the original.">
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||
Save As...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
hasStorePermission && currentSavedView != null &&
|
||||
<Tooltip {...menuTooltipAttribs} title="Delete this saved view.">
|
||||
<MenuItem disabled={currentSavedView === null} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||
Delete...
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
<Tooltip {...menuTooltipAttribs} title="Create a new view of this table, resetting the filters and columns to their defaults.">
|
||||
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
||||
New View
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
}
|
||||
<Divider/>
|
||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Views</b></MenuItem>
|
||||
{
|
||||
savedViews && savedViews.length > 0 ? (
|
||||
savedViews.map((record: QRecord, index: number) =>
|
||||
<MenuItem sx={{paddingLeft: "50px"}} key={`savedFiler-${index}`} onClick={() => handleSavedViewRecordOnClick(record)}>
|
||||
{record.values.get("label")}
|
||||
</MenuItem>
|
||||
)
|
||||
): (
|
||||
<MenuItem>
|
||||
<i>You do not have any saved views for this table.</i>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
let buttonText = "Views";
|
||||
let buttonBackground = "none";
|
||||
let buttonBorder = colors.grayLines.main;
|
||||
let buttonColor = colors.gray.main;
|
||||
|
||||
if(currentSavedView)
|
||||
{
|
||||
if (viewIsModified)
|
||||
{
|
||||
buttonBackground = accentColorLight;
|
||||
buttonBorder = buttonBackground;
|
||||
buttonColor = accentColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonBackground = accentColor;
|
||||
buttonBorder = buttonBackground;
|
||||
buttonColor = "#FFFFFF";
|
||||
}
|
||||
}
|
||||
|
||||
const buttonStyles = {
|
||||
border: `1px solid ${buttonBorder}`,
|
||||
backgroundColor: buttonBackground,
|
||||
color: buttonColor,
|
||||
"&:focus:not(:hover)": {
|
||||
color: buttonColor,
|
||||
backgroundColor: buttonBackground,
|
||||
},
|
||||
"&:hover": {
|
||||
color: buttonColor,
|
||||
backgroundColor: buttonBackground,
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function isSaveButtonDisabled(): boolean
|
||||
{
|
||||
if(isSubmitting)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
const haveInputText = (savedViewNameInputValue != null && savedViewNameInputValue.trim() != "")
|
||||
|
||||
if(isSaveFilterAs || isRenameFilter || currentSavedView == null)
|
||||
{
|
||||
if(!haveInputText)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
const linkButtonStyle = {
|
||||
minWidth: "unset",
|
||||
textTransform: "none",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "500",
|
||||
padding: "0.5rem"
|
||||
};
|
||||
|
||||
return (
|
||||
hasQueryPermission && tableMetaData ? (
|
||||
<>
|
||||
<Box order="1" mr={"0.5rem"}>
|
||||
<Button
|
||||
onClick={openSavedViewsMenu}
|
||||
sx={{
|
||||
borderRadius: "0.75rem",
|
||||
textTransform: "none",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.875rem",
|
||||
p: "0.5rem",
|
||||
... buttonStyles
|
||||
}}
|
||||
>
|
||||
<Icon sx={{mr: "0.5rem"}}>save</Icon>
|
||||
{buttonText}
|
||||
<Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon>
|
||||
</Button>
|
||||
{renderSavedViewsMenu}
|
||||
</Box>
|
||||
<Box order="3" display="flex" justifyContent="center" flexDirection="column">
|
||||
<Box pl={2} pr={2} sx={{display: "flex", alignItems: "center"}}>
|
||||
{
|
||||
!currentSavedView && viewIsModified && <>
|
||||
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
|
||||
<b>Unsaved Changes</b>
|
||||
<ul style={{padding: "0.5rem 1rem"}}>
|
||||
{
|
||||
viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)
|
||||
}
|
||||
</ul>
|
||||
</>}>
|
||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save View As…</Button>
|
||||
</Tooltip>
|
||||
|
||||
{/* vertical rule */}
|
||||
<Box display="inline-block" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" />
|
||||
|
||||
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>Reset All Changes</Button>
|
||||
</>
|
||||
}
|
||||
{
|
||||
currentSavedView && viewIsModified && <>
|
||||
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
|
||||
<b>Unsaved Changes</b>
|
||||
<ul style={{padding: "0.5rem 1rem"}}>
|
||||
{
|
||||
viewDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)
|
||||
}
|
||||
</ul></>}>
|
||||
<Box display="inline" sx={{...linkButtonStyle, p: 0, cursor: "default", position: "relative", top: "-1px"}}>{viewDiffs.length} Unsaved Change{viewDiffs.length == 1 ? "" : "s"}</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save…</Button>
|
||||
|
||||
{/* vertical rule */}
|
||||
<Box display="inline-block" borderLeft={`1px solid ${colors.grayLines.main}`} height="1rem" width="1px" position="relative" />
|
||||
|
||||
<Button disableRipple={true} sx={{color: colors.gray.main, ... linkButtonStyle}} onClick={() => handleSavedViewRecordOnClick(currentSavedView)}>Reset All Changes</Button>
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{
|
||||
<Dialog
|
||||
open={saveFilterPopupOpen}
|
||||
onClose={handleSaveFilterPopupClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyPress={(e) =>
|
||||
{
|
||||
////////////////////////////////////////////////////
|
||||
// make user actually hit delete button //
|
||||
// but for other modes, let Enter submit the form //
|
||||
////////////////////////////////////////////////////
|
||||
if (e.key == "Enter" && !isDeleteFilter)
|
||||
{
|
||||
handleFilterDialogButtonOnClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{
|
||||
currentSavedView ? (
|
||||
isDeleteFilter ? (
|
||||
<DialogTitle id="alert-dialog-title">Delete View</DialogTitle>
|
||||
) : (
|
||||
isSaveFilterAs ? (
|
||||
<DialogTitle id="alert-dialog-title">Save View As</DialogTitle>
|
||||
):(
|
||||
isRenameFilter ? (
|
||||
<DialogTitle id="alert-dialog-title">Rename View</DialogTitle>
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Update Existing View</DialogTitle>
|
||||
)
|
||||
)
|
||||
)
|
||||
):(
|
||||
<DialogTitle id="alert-dialog-title">Save New View</DialogTitle>
|
||||
)
|
||||
}
|
||||
<DialogContent sx={{width: "500px"}}>
|
||||
{popupAlertContent ? (
|
||||
<Box mb={1}>
|
||||
<Alert severity="error" onClose={() => setPopupAlertContent("")}>{popupAlertContent}</Alert>
|
||||
</Box>
|
||||
) : ("")}
|
||||
{
|
||||
(! currentSavedView || isSaveFilterAs || isRenameFilter) && ! isDeleteFilter ? (
|
||||
<Box>
|
||||
{
|
||||
isSaveFilterAs ? (
|
||||
<Box mb={3}>Enter a name for this new saved view.</Box>
|
||||
):(
|
||||
<Box mb={3}>Enter a new name for this saved view.</Box>
|
||||
)
|
||||
}
|
||||
<TextField
|
||||
autoFocus
|
||||
name="custom-delimiter-value"
|
||||
placeholder="View Name"
|
||||
inputProps={{width: "100%", maxLength: 100}}
|
||||
value={savedViewNameInputValue}
|
||||
sx={{width: "100%"}}
|
||||
onChange={handleSaveFilterInputChange}
|
||||
onFocus={event =>
|
||||
{
|
||||
event.target.select();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
):(
|
||||
isDeleteFilter ? (
|
||||
<Box>Are you sure you want to delete the view {`'${currentSavedView?.values.get("label")}'`}?</Box>
|
||||
):(
|
||||
<Box>Are you sure you want to update the view {`'${currentSavedView?.values.get("label")}'`}?</Box>
|
||||
)
|
||||
)
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<QCancelButton onClickHandler={handleSaveFilterPopupClose} disabled={false} />
|
||||
{
|
||||
isDeleteFilter ?
|
||||
<QDeleteButton onClickHandler={handleFilterDialogButtonOnClick} disabled={isSubmitting} />
|
||||
:
|
||||
<QSaveButton label="Save" onClickHandler={handleFilterDialogButtonOnClick} disabled={isSaveButtonDisabled()}/>
|
||||
}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
}
|
||||
</>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default SavedViews;
|
@ -28,12 +28,11 @@ interface TabPanelProps
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export default function TabPanel(props: TabPanelProps)
|
||||
{
|
||||
const {children, value, index, style, ...other} = props;
|
||||
const {children, value, index, ...other} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -41,7 +40,6 @@ export default function TabPanel(props: TabPanelProps)
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
style={style}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
|
@ -155,7 +155,7 @@ function ValidationReview({
|
||||
"false",
|
||||
"Skip Validation. Submit the records for immediate processing", (
|
||||
<div>
|
||||
If you choose this option, the input records will immediately be processed.
|
||||
If you choose this option, the records input records will immediately be processed.
|
||||
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
||||
<br />
|
||||
<br />
|
||||
|
@ -1,837 +0,0 @@
|
||||
/*
|
||||
* 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 {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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Badge, ToggleButton, ToggleButtonGroup} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {GridApiPro} from "@mui/x-data-grid-pro/models/gridApiPro";
|
||||
import React, {forwardRef, useContext, useImperativeHandle, useReducer, useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||
import FieldListMenu from "qqq/components/query/FieldListMenu";
|
||||
import {validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||
import QuickFilter, {quickFilterButtonStyles} from "qqq/components/query/QuickFilter";
|
||||
import XIcon from "qqq/components/query/XIcon";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
|
||||
interface BasicAndAdvancedQueryControlsProps
|
||||
{
|
||||
metaData: QInstance;
|
||||
tableMetaData: QTableMetaData;
|
||||
|
||||
savedViewsComponent: JSX.Element;
|
||||
columnMenuComponent: JSX.Element;
|
||||
|
||||
quickFilterFieldNames: string[];
|
||||
setQuickFilterFieldNames: (names: string[]) => void;
|
||||
|
||||
queryFilter: QQueryFilter;
|
||||
setQueryFilter: (queryFilter: QQueryFilter) => void;
|
||||
|
||||
gridApiRef: React.MutableRefObject<GridApiPro>;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this prop is used as a way to recognize changes in the query filter internal structure, //
|
||||
// since the queryFilter object (reference) doesn't get updated //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
queryFilterJSON: string;
|
||||
|
||||
mode: string;
|
||||
setMode: (mode: string) => void;
|
||||
}
|
||||
|
||||
let debounceTimeout: string | number | NodeJS.Timeout;
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to provide the basic & advanced query-filter controls for the
|
||||
** RecordQueryOrig screen.
|
||||
**
|
||||
** Done as a forwardRef, so RecordQueryOrig can call some functions, e.g., when user
|
||||
** does things on that screen, that we need to know about in here.
|
||||
*******************************************************************************/
|
||||
const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryControlsProps, ref) =>
|
||||
{
|
||||
const {metaData, tableMetaData, savedViewsComponent, columnMenuComponent, quickFilterFieldNames, setQuickFilterFieldNames, setQueryFilter, queryFilter, gridApiRef, queryFilterJSON, mode, setMode} = props
|
||||
|
||||
/////////////////////
|
||||
// state variables //
|
||||
/////////////////////
|
||||
const [defaultQuickFilterFieldNames, setDefaultQuickFilterFieldNames] = useState(getDefaultQuickFilterFieldNames(tableMetaData));
|
||||
const [defaultQuickFilterFieldNameMap, setDefaultQuickFilterFieldNameMap] = useState(Object.fromEntries(defaultQuickFilterFieldNames.map(k => [k, true])));
|
||||
const [addQuickFilterMenu, setAddQuickFilterMenu] = useState(null)
|
||||
const [addQuickFilterOpenCounter, setAddQuickFilterOpenCounter] = useState(0);
|
||||
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
|
||||
const [mouseOverElement, setMouseOverElement] = useState(null as string);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// make some functions available to our parent - so it can tell us to do things //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
useImperativeHandle(ref, () =>
|
||||
{
|
||||
return {
|
||||
ensureAllFilterCriteriaAreActiveQuickFilters(currentFilter: QQueryFilter, reason: string)
|
||||
{
|
||||
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, currentFilter, reason);
|
||||
},
|
||||
addField(fieldName: string)
|
||||
{
|
||||
addQuickFilterField({fieldName: fieldName}, "columnMenu");
|
||||
},
|
||||
getCurrentMode()
|
||||
{
|
||||
return (mode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function handleMouseOverElement(name: string)
|
||||
{
|
||||
setMouseOverElement(name);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function handleMouseOutElement()
|
||||
{
|
||||
setMouseOverElement(null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for a given field, set its default operator for quick-filter dropdowns.
|
||||
*******************************************************************************/
|
||||
function getDefaultOperatorForField(field: QFieldMetaData)
|
||||
{
|
||||
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
||||
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
||||
{
|
||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||
}
|
||||
else if (field?.type == QFieldType.BOOLEAN)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for booleans, if we set a default, since none of them have values, then they are ALWAYS selected, which isn't what we want. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
defaultOperator = null;
|
||||
}
|
||||
return defaultOperator;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Callback passed into the QuickFilter component, to update the criteria
|
||||
** after user makes changes to it or to clear it out.
|
||||
*******************************************************************************/
|
||||
const updateQuickCriteria = (newCriteria: QFilterCriteria, needDebounce = false, doClearCriteria = false) =>
|
||||
{
|
||||
let found = false;
|
||||
let foundIndex = null;
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
if(queryFilter.criteria[i].fieldName == newCriteria.fieldName)
|
||||
{
|
||||
queryFilter.criteria[i] = newCriteria;
|
||||
found = true;
|
||||
foundIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(doClearCriteria)
|
||||
{
|
||||
if(found)
|
||||
{
|
||||
queryFilter.criteria.splice(foundIndex, 1);
|
||||
setQueryFilter(queryFilter);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(!found)
|
||||
{
|
||||
if(!queryFilter.criteria)
|
||||
{
|
||||
queryFilter.criteria = [];
|
||||
}
|
||||
queryFilter.criteria.push(newCriteria);
|
||||
found = true;
|
||||
}
|
||||
|
||||
if(found)
|
||||
{
|
||||
clearTimeout(debounceTimeout)
|
||||
debounceTimeout = setTimeout(() =>
|
||||
{
|
||||
setQueryFilter(queryFilter);
|
||||
}, needDebounce ? 500 : 1);
|
||||
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the QFilterCriteriaWithId object to pass in to the QuickFilter component
|
||||
** for a given field name.
|
||||
*******************************************************************************/
|
||||
const getQuickCriteriaParam = (fieldName: string): QFilterCriteriaWithId | null | "tooComplex" =>
|
||||
{
|
||||
const matches: QFilterCriteriaWithId[] = [];
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
if(queryFilter.criteria[i].fieldName == fieldName)
|
||||
{
|
||||
matches.push(queryFilter.criteria[i] as QFilterCriteriaWithId);
|
||||
}
|
||||
}
|
||||
|
||||
if(matches.length == 0)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
else if(matches.length == 1)
|
||||
{
|
||||
return (matches[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "tooComplex";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for QuickFilter component, to remove a quick filter field from
|
||||
** the screen.
|
||||
*******************************************************************************/
|
||||
const handleRemoveQuickFilterField = (fieldName: string): void =>
|
||||
{
|
||||
const index = quickFilterFieldNames.indexOf(fieldName)
|
||||
if(index >= 0)
|
||||
{
|
||||
//////////////////////////////////////
|
||||
// remove this field from the query //
|
||||
//////////////////////////////////////
|
||||
const criteria = new QFilterCriteria(fieldName, null, []);
|
||||
updateQuickCriteria(criteria, false, true);
|
||||
|
||||
quickFilterFieldNames.splice(index, 1);
|
||||
setQuickFilterFieldNames(quickFilterFieldNames);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for button that opens the add-quick-filter menu
|
||||
*******************************************************************************/
|
||||
const openAddQuickFilterMenu = (event: any) =>
|
||||
{
|
||||
setAddQuickFilterMenu(event.currentTarget);
|
||||
setAddQuickFilterOpenCounter(addQuickFilterOpenCounter + 1);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Handle closing the add-quick-filter menu
|
||||
*******************************************************************************/
|
||||
const closeAddQuickFilterMenu = () =>
|
||||
{
|
||||
setAddQuickFilterMenu(null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a quick-filter field to the screen, from either the user selecting one,
|
||||
** or from a new query being activated, etc.
|
||||
*******************************************************************************/
|
||||
const addQuickFilterField = (newValue: any, reason: "blur" | "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | "columnMenu" | "activatedView" | string) =>
|
||||
{
|
||||
console.log(`Adding quick filter field as: ${JSON.stringify(newValue)}`);
|
||||
if (reason == "blur")
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// this keeps a click out of the menu from selecting the option //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldName = newValue ? newValue.fieldName : null;
|
||||
if (fieldName)
|
||||
{
|
||||
if(defaultQuickFilterFieldNameMap[fieldName])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (quickFilterFieldNames.indexOf(fieldName) == -1)
|
||||
{
|
||||
/////////////////////////////////
|
||||
// add the field if we need to //
|
||||
/////////////////////////////////
|
||||
quickFilterFieldNames.push(fieldName);
|
||||
setQuickFilterFieldNames(quickFilterFieldNames);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// only do this when user has added the field (e.g., not when adding it because of a selected view or filter-in-url) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(reason != "modeToggleClicked" && reason != "defaultFilterLoaded" && reason != "savedFilterSelected" && reason != "activatedView")
|
||||
{
|
||||
setTimeout(() => document.getElementById(`quickFilter.${fieldName}`)?.click(), 5);
|
||||
}
|
||||
}
|
||||
else if(reason == "columnMenu")
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if field was already on-screen, but user clicked an option from the columnMenu, then open the quick-filter field //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
setTimeout(() => document.getElementById(`quickFilter.${fieldName}`)?.click(), 5);
|
||||
}
|
||||
|
||||
closeAddQuickFilterMenu();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const handleFieldListMenuSelection = (field: QFieldMetaData, table: QTableMetaData): void =>
|
||||
{
|
||||
let fullFieldName = field.name;
|
||||
if(table && table.name != tableMetaData.name)
|
||||
{
|
||||
fullFieldName = `${table.name}.${field.name}`;
|
||||
}
|
||||
|
||||
addQuickFilterField({fieldName: fullFieldName}, "selectedFromAddFilterMenu");
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for the Filter Builder button - e.g., opens the parent's grid's
|
||||
** filter panel
|
||||
*******************************************************************************/
|
||||
const openFilterBuilder = (e: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>) =>
|
||||
{
|
||||
gridApiRef.current.showFilterPanel();
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for the clear-filters modal
|
||||
*******************************************************************************/
|
||||
const handleClearFiltersAction = (event: React.KeyboardEvent<HTMLDivElement>, isYesButton: boolean = false) =>
|
||||
{
|
||||
if (isYesButton || event.key == "Enter")
|
||||
{
|
||||
setShowClearFiltersWarning(false);
|
||||
setQueryFilter(new QQueryFilter([], queryFilter.orderBys));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const removeCriteriaByIndex = (index: number) =>
|
||||
{
|
||||
queryFilter.criteria.splice(index, 1);
|
||||
setQueryFilter(queryFilter);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** format the current query as a string for showing on-screen as a preview.
|
||||
*******************************************************************************/
|
||||
const queryToAdvancedString = () =>
|
||||
{
|
||||
if(queryFilter == null || !queryFilter.criteria)
|
||||
{
|
||||
return (<span></span>);
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
|
||||
return (
|
||||
<Box display="flex" flexWrap="wrap" fontSize="0.875rem">
|
||||
{queryFilter.criteria.map((criteria, i) =>
|
||||
{
|
||||
const {criteriaIsValid} = validateCriteria(criteria, null);
|
||||
if(criteriaIsValid)
|
||||
{
|
||||
counter++;
|
||||
|
||||
return (
|
||||
<span key={i} style={{marginBottom: "0.125rem"}} onMouseOver={() => handleMouseOverElement(`queryPreview-${i}`)} onMouseOut={() => handleMouseOutElement()}>
|
||||
{counter > 1 ? <span style={{marginLeft: "0.25rem", marginRight: "0.25rem"}}>{queryFilter.booleanOperator} </span> : <span/>}
|
||||
{FilterUtils.criteriaToHumanString(tableMetaData, criteria, true)}
|
||||
{mouseOverElement == `queryPreview-${i}` && <span className={`advancedQueryPreviewX-${counter - 1}`}><XIcon position="forAdvancedQueryPreview" onClick={() => removeCriteriaByIndex(i)} /></span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<span />);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for toggling between modes - basic & advanced.
|
||||
*******************************************************************************/
|
||||
const modeToggleClicked = (newValue: string | null) =>
|
||||
{
|
||||
if (newValue)
|
||||
{
|
||||
if(newValue == "basic")
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// we're always allowed to go to advanced - //
|
||||
// but if we're trying to go to basic, make sure the filter isn't too complex //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const {canFilterWorkAsBasic} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||
if (!canFilterWorkAsBasic)
|
||||
{
|
||||
console.log("Query cannot work as basic - so - not allowing toggle to basic.")
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// when going to basic, make sure all fields in the current query are active as quick-filters //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (queryFilter && queryFilter.criteria)
|
||||
{
|
||||
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "modeToggleClicked", "basic");
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - this is a callback to the parent - as it is responsible for this state... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
setMode(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** make sure that all fields in the current query are on-screen as quick-filters
|
||||
** (that is, if the query can be basic)
|
||||
*******************************************************************************/
|
||||
const ensureAllFilterCriteriaAreActiveQuickFilters = (tableMetaData: QTableMetaData, queryFilter: QQueryFilter, reason: "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | string, newMode?: string) =>
|
||||
{
|
||||
if(!tableMetaData || !queryFilter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const {canFilterWorkAsBasic} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||
if (!canFilterWorkAsBasic)
|
||||
{
|
||||
console.log("query is too complex for basic - so - switching to advanced");
|
||||
modeToggleClicked("advanced");
|
||||
forceUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const modeToUse = newMode ?? mode;
|
||||
if(modeToUse == "basic")
|
||||
{
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
const criteria = queryFilter.criteria[i];
|
||||
if (criteria && criteria.fieldName)
|
||||
{
|
||||
addQuickFilterField(criteria, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** count how many valid criteria are in the query - for showing badge
|
||||
*******************************************************************************/
|
||||
const countValidCriteria = (queryFilter: QQueryFilter): number =>
|
||||
{
|
||||
let count = 0;
|
||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
const {criteriaIsValid} = validateCriteria(queryFilter.criteria[i], null);
|
||||
if(criteriaIsValid)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for setting the sort from that menu
|
||||
*******************************************************************************/
|
||||
const handleSetSort = (field: QFieldMetaData, table: QTableMetaData, isAscending: boolean = true): void =>
|
||||
{
|
||||
const fullFieldName = table && table.name != tableMetaData.name ? `${table.name}.${field.name}` : field.name;
|
||||
queryFilter.orderBys = [new QFilterOrderBy(fullFieldName, isAscending)]
|
||||
|
||||
setQueryFilter(queryFilter);
|
||||
forceUpdate();
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for a click on a field's up or down arrow in the sort menu
|
||||
*******************************************************************************/
|
||||
const handleSetSortArrowClick = (field: QFieldMetaData, table: QTableMetaData, event: any): void =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure this is an event handler for one of our icons (not something else in the dom here in our end-adornments) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const isAscending = event.target.innerHTML == "arrow_upward";
|
||||
const isDescending = event.target.innerHTML == "arrow_downward";
|
||||
if(isAscending || isDescending)
|
||||
{
|
||||
handleSetSort(field, table, isAscending);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for clicking the current sort up/down arrow, to toggle direction.
|
||||
*******************************************************************************/
|
||||
function toggleSortDirection(event: React.MouseEvent<HTMLSpanElement, MouseEvent>): void
|
||||
{
|
||||
event.stopPropagation();
|
||||
try
|
||||
{
|
||||
queryFilter.orderBys[0].isAscending = !queryFilter.orderBys[0].isAscending;
|
||||
setQueryFilter(queryFilter);
|
||||
forceUpdate();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(`Error toggling sort: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// set up the sort menu button //
|
||||
/////////////////////////////////
|
||||
let sortButtonContents = <>Sort...</>
|
||||
if(queryFilter && queryFilter.orderBys && queryFilter.orderBys.length > 0)
|
||||
{
|
||||
const orderBy = queryFilter.orderBys[0];
|
||||
const orderByFieldName = orderBy.fieldName;
|
||||
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, orderByFieldName);
|
||||
const fieldLabel = fieldTable.name == tableMetaData.name ? field.label : `${fieldTable.label}: ${field.label}`;
|
||||
sortButtonContents = <>Sort: {fieldLabel} <Icon onClick={toggleSortDirection} sx={{ml: "0.5rem"}}>{orderBy.isAscending ? "arrow_upward" : "arrow_downward"}</Icon></>
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is being used as a version of like forcing that we get re-rendered if the query filter changes... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const [lastIndex, setLastIndex] = useState(queryFilterJSON);
|
||||
if(queryFilterJSON != lastIndex)
|
||||
{
|
||||
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "defaultFilterLoaded");
|
||||
setLastIndex(queryFilterJSON);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// set some status flags based on current filter //
|
||||
///////////////////////////////////////////////////
|
||||
const hasValidFilters = queryFilter && countValidCriteria(queryFilter) > 0;
|
||||
const {canFilterWorkAsBasic, reasonsWhyItCannot} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||
let reasonWhyBasicIsDisabled = null;
|
||||
if(reasonsWhyItCannot && reasonsWhyItCannot.length > 0)
|
||||
{
|
||||
reasonWhyBasicIsDisabled = <>
|
||||
Your current Filter cannot be managed using Basic mode because:
|
||||
<ul style={{marginLeft: "1rem"}}>
|
||||
{reasonsWhyItCannot.map((reason, i) => <li key={i}>{reason}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
|
||||
const borderGray = colors.grayLines.main;
|
||||
|
||||
const sortMenuComponent = (
|
||||
<FieldListMenu
|
||||
idPrefix="sort"
|
||||
tableMetaData={tableMetaData}
|
||||
placeholder="Search Fields"
|
||||
buttonProps={{disableRipple: true, sx: {textTransform: "none", color: colors.gray.main, paddingRight: 0}}}
|
||||
buttonChildren={sortButtonContents}
|
||||
isModeSelectOne={true}
|
||||
handleSelectedField={handleSetSort}
|
||||
fieldEndAdornment={<Box whiteSpace="nowrap"><Icon>arrow_upward</Icon><Icon>arrow_downward</Icon></Box>}
|
||||
handleAdornmentClick={handleSetSortArrowClick}
|
||||
/>);
|
||||
|
||||
const filterBuilderMouseEvents =
|
||||
{
|
||||
onMouseOver: () => handleMouseOverElement("filterBuilderButton"),
|
||||
onMouseOut: () => handleMouseOutElement()
|
||||
};
|
||||
|
||||
return (
|
||||
<Box pb={mode == "advanced" ? "0.25rem" : "0"}>
|
||||
|
||||
{/* First row: Saved Views button (with Columns button in the middle of it), then space-between, then basic|advanced toggle */}
|
||||
<Box display="flex" justifyContent="space-between" pt={"0.5rem"} pb={"0.5rem"}>
|
||||
<Box display="flex">
|
||||
{savedViewsComponent}
|
||||
{columnMenuComponent}
|
||||
</Box>
|
||||
<Box>
|
||||
<Tooltip title={reasonWhyBasicIsDisabled}>
|
||||
<ToggleButtonGroup
|
||||
value={mode}
|
||||
exclusive
|
||||
onChange={(event, newValue) => modeToggleClicked(newValue)}
|
||||
size="small"
|
||||
sx={{pl: 0.5, width: "10rem"}}
|
||||
>
|
||||
<ToggleButton value="basic" disabled={!canFilterWorkAsBasic}>Basic</ToggleButton>
|
||||
<ToggleButton value="advanced">Advanced</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Second row: Basic or advanced mode - with sort-by control on the right (of each) */}
|
||||
<Box pb={"0.25rem"}>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// basic mode - wrapping-list of fields & add-field button, then sort-by control //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
mode == "basic" &&
|
||||
<Box display="flex" alignItems="flex-start" flexShrink={1} flexGrow={1}>
|
||||
<Box width="100px" flexShrink={1} flexGrow={1}>
|
||||
<>
|
||||
{
|
||||
tableMetaData && defaultQuickFilterFieldNames?.map((fieldName) =>
|
||||
{
|
||||
const [field] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
let defaultOperator = getDefaultOperatorForField(field);
|
||||
|
||||
return (<QuickFilter
|
||||
key={fieldName}
|
||||
fullFieldName={fieldName}
|
||||
tableMetaData={tableMetaData}
|
||||
updateCriteria={updateQuickCriteria}
|
||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||
fieldMetaData={field}
|
||||
defaultOperator={defaultOperator}
|
||||
handleRemoveQuickFilterField={null} />);
|
||||
})
|
||||
}
|
||||
{/* vertical rule */}
|
||||
<Box display="inline-block" borderLeft={`1px solid ${borderGray}`} height="1.75rem" width="1px" marginRight="0.5rem" position="relative" top="0.5rem" />
|
||||
{
|
||||
tableMetaData && quickFilterFieldNames?.map((fieldName) =>
|
||||
{
|
||||
const [field] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
let defaultOperator = getDefaultOperatorForField(field);
|
||||
|
||||
return (defaultQuickFilterFieldNameMap[fieldName] ? null : <QuickFilter
|
||||
key={fieldName}
|
||||
fullFieldName={fieldName}
|
||||
tableMetaData={tableMetaData}
|
||||
updateCriteria={updateQuickCriteria}
|
||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||
fieldMetaData={field}
|
||||
defaultOperator={defaultOperator}
|
||||
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />);
|
||||
})
|
||||
}
|
||||
{
|
||||
tableMetaData && <FieldListMenu
|
||||
key={JSON.stringify(quickFilterFieldNames)} // use a unique key each time we open it, because we don't want the user's last selection to stick.
|
||||
idPrefix="addQuickFilter"
|
||||
tableMetaData={tableMetaData}
|
||||
fieldNamesToHide={[...(defaultQuickFilterFieldNames ?? []), ...(quickFilterFieldNames ?? [])]}
|
||||
placeholder="Search Fields"
|
||||
buttonProps={{sx: quickFilterButtonStyles, startIcon: (<Icon>add</Icon>)}}
|
||||
buttonChildren={"Add Filter"}
|
||||
isModeSelectOne={true}
|
||||
handleSelectedField={handleFieldListMenuSelection}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
</Box>
|
||||
<Box>
|
||||
{sortMenuComponent}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// advanced mode - 2 rows - one for Filter Builder button & sort control, 2nd row for the filter-detail box //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
metaData && tableMetaData && mode == "advanced" &&
|
||||
<Box borderRadius="0.75rem" border={`1px solid ${borderGray}`}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box p="0.5rem">
|
||||
<Tooltip enterDelay={500} title="Build an advanced Filter" placement="top">
|
||||
<>
|
||||
<Button
|
||||
className="filterBuilderButton"
|
||||
onClick={(e) => openFilterBuilder(e)}
|
||||
{... filterBuilderMouseEvents}
|
||||
startIcon={<Icon>build</Icon>}
|
||||
sx={{borderRadius: "0.75rem", padding: "0.5rem", pl: "1rem", fontSize: "0.875rem", fontWeight: 500, border: `1px solid ${accentColor}`, textTransform: "none"}}
|
||||
>
|
||||
Filter Builder
|
||||
{
|
||||
countValidCriteria(queryFilter) > 0 &&
|
||||
<Box {... filterBuilderMouseEvents} sx={{backgroundColor: accentColor, marginLeft: "0.25rem", minWidth: "1rem", fontSize: "0.75rem"}} borderRadius="50%" color="#FFFFFF" position="relative" top="-2px" className="filterBuilderCountBadge">
|
||||
{countValidCriteria(queryFilter) }
|
||||
</Box>
|
||||
}
|
||||
</Button>
|
||||
{
|
||||
hasValidFilters && mouseOverElement == "filterBuilderButton" && <span {... filterBuilderMouseEvents} className="filterBuilderXIcon"><XIcon shade="accent" position="default" onClick={() => setShowClearFiltersWarning(true)} /></span>
|
||||
}
|
||||
</>
|
||||
</Tooltip>
|
||||
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)} onKeyPress={(e) => handleClearFiltersAction(e)}>
|
||||
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Are you sure you want to remove all conditions from the current filter?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(false)} />
|
||||
<QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() => handleClearFiltersAction(null, true)} />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
<Box pr={"0.5rem"}>
|
||||
{sortMenuComponent}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box whiteSpace="nowrap" display="flex" flexShrink={1} flexGrow={1} alignItems="center">
|
||||
{
|
||||
<Box
|
||||
className="advancedQueryString"
|
||||
display="inline-block"
|
||||
borderTop={`1px solid ${borderGray}`}
|
||||
borderRadius="0 0 0.75rem 0.75rem"
|
||||
width="100%"
|
||||
sx={{fontSize: "1rem", background: "#FFFFFF"}}
|
||||
minHeight={"2.375rem"}
|
||||
p={"0.5rem"}
|
||||
pb={"0.125rem"}
|
||||
boxShadow={"inset 0px 0px 4px 2px #EFEFED"}
|
||||
>
|
||||
{queryToAdvancedString()}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export function getDefaultQuickFilterFieldNames(table: QTableMetaData): string[]
|
||||
{
|
||||
const defaultQuickFilterFieldNames: string[] = [];
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if there's materialDashboard tableMetaData, and if it has defaultQuickFilterFieldNames //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const mdbMetaData = table?.supplementalTableMetaData?.get("materialDashboard");
|
||||
if (mdbMetaData)
|
||||
{
|
||||
if (mdbMetaData?.defaultQuickFilterFieldNames?.length)
|
||||
{
|
||||
for (let i = 0; i < mdbMetaData.defaultQuickFilterFieldNames.length; i++)
|
||||
{
|
||||
defaultQuickFilterFieldNames.push(mdbMetaData.defaultQuickFilterFieldNames[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// if still none, then look for T1 section //
|
||||
/////////////////////////////////////////////
|
||||
if (defaultQuickFilterFieldNames.length == 0)
|
||||
{
|
||||
if (table.sections)
|
||||
{
|
||||
const t1Sections = table.sections.filter((s: QTableSection) => s.tier == "T1");
|
||||
if (t1Sections.length)
|
||||
{
|
||||
for (let i = 0; i < t1Sections.length; i++)
|
||||
{
|
||||
if (t1Sections[i].fieldNames)
|
||||
{
|
||||
for (let j = 0; j < t1Sections[i].fieldNames.length; j++)
|
||||
{
|
||||
defaultQuickFilterFieldNames.push(t1Sections[i].fieldNames[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (defaultQuickFilterFieldNames);
|
||||
}
|
||||
|
||||
export default BasicAndAdvancedQueryControls;
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {TablePagination} from "@mui/material";
|
||||
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";
|
||||
|
||||
interface CustomPaginationProps
|
||||
{
|
||||
tableMetaData: QTableMetaData;
|
||||
rows: GridRowsProp[];
|
||||
totalRecords: number;
|
||||
distinctRecords: number;
|
||||
pageNumber: number;
|
||||
rowsPerPage: number;
|
||||
loading: boolean;
|
||||
isJoinMany: boolean;
|
||||
handlePageChange: (value: number) => void;
|
||||
handleRowsPerPageChange: (value: number) => void;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** DataGrid custom component - for pagination!
|
||||
*******************************************************************************/
|
||||
export default function CustomPaginationComponent({tableMetaData, rows, totalRecords, distinctRecords, pageNumber, rowsPerPage, loading, isJoinMany, handlePageChange, handleRowsPerPageChange}: CustomPaginationProps): JSX.Element
|
||||
{
|
||||
// @ts-ignore
|
||||
const defaultLabelDisplayedRows = ({from, to, count}) =>
|
||||
{
|
||||
const tooltipHTML = <>
|
||||
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>
|
||||
</CustomWidthTooltip>
|
||||
)
|
||||
</Box>) : <></>;
|
||||
|
||||
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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;
|
||||
}
|
||||
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// treat -1 as the sentinel that it's set as below -- remember, we did that so that 'to' would have a value in here when there's no count. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (count !== null && count !== undefined && count !== -1)
|
||||
{
|
||||
if (count === 0)
|
||||
{
|
||||
return (loading ? "Counting..." : "No rows");
|
||||
}
|
||||
|
||||
return <span>
|
||||
Showing {from.toLocaleString()} to {to.toLocaleString()} of
|
||||
{
|
||||
count == -1 ?
|
||||
<>more than {to.toLocaleString()}</>
|
||||
: <> {count.toLocaleString()}{distinctPart}</>
|
||||
}
|
||||
</span>;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ("Counting...");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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}
|
||||
page={pageNumber}
|
||||
rowsPerPageOptions={[10, 25, 50, 100, 250]}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={(event, value) => handlePageChange(value)}
|
||||
onRowsPerPageChange={(event) => handleRowsPerPageChange(Number(event.target.value))}
|
||||
labelDisplayedRows={defaultLabelDisplayedRows}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
/*
|
||||
* 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {GridColDef, GridExportMenuItemProps} from "@mui/x-data-grid-pro";
|
||||
import React from "react";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
|
||||
{
|
||||
tableMetaData: QTableMetaData;
|
||||
totalRecords: number
|
||||
columnsModel: GridColDef[];
|
||||
columnVisibilityModel: { [index: string]: boolean };
|
||||
queryFilter: QQueryFilter;
|
||||
format: string;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to serve as an item in the Export menu
|
||||
*******************************************************************************/
|
||||
export default function ExportMenuItem(props: QExportMenuItemProps)
|
||||
{
|
||||
const {format, tableMetaData, totalRecords, columnsModel, columnVisibilityModel, queryFilter, hideMenu} = props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={!totalRecords}
|
||||
onClick={() =>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// build the list of visible fields. note, not doing them in-order (in case //
|
||||
// the user did drag & drop), because column order model isn't right yet //
|
||||
// so just doing them to match columns (which were pKey, then sorted) //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
const visibleFields: string[] = [];
|
||||
columnsModel.forEach((gridColumn) =>
|
||||
{
|
||||
const fieldName = gridColumn.field;
|
||||
if (columnVisibilityModel[fieldName] !== false)
|
||||
{
|
||||
visibleFields.push(fieldName);
|
||||
}
|
||||
});
|
||||
|
||||
//////////////////////////////////////
|
||||
// construct the url for the export //
|
||||
//////////////////////////////////////
|
||||
const dateString = ValueUtils.formatDateTimeForFileName(new Date());
|
||||
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
||||
const url = `/data/${tableMetaData.name}/export/${filename}`;
|
||||
|
||||
const encodedFilterJSON = encodeURIComponent(JSON.stringify(queryFilter));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// open a window (tab) with a little page that says the file is being generated. //
|
||||
// then have that page load the url for the export. //
|
||||
// If there's an error, it'll appear in that window. else, the file will download. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
const exportWindow = window.open("", "_blank");
|
||||
exportWindow.document.write(`<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
* { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
|
||||
</style>
|
||||
<title>${filename}</title>
|
||||
<script>
|
||||
setTimeout(() =>
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// need to encode and decode this value, so set it in the form here, instead of literally below //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
document.getElementById("filter").value = decodeURIComponent("${encodedFilterJSON}");
|
||||
|
||||
document.getElementById("exportForm").submit();
|
||||
}, 1);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||
<form id="exportForm" method="post" action="${url}" >
|
||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||
<input type="hidden" name="filter" id="filter">
|
||||
</form>
|
||||
</body>
|
||||
</html>`);
|
||||
|
||||
/*
|
||||
// todo - probably better - generate the report in an iframe...
|
||||
// only open question is, giving user immediate feedback, and knowing when the stream has started and/or stopped
|
||||
// maybe a busy-loop that would check iframe's url (e.g., after posting should change, maybe?)
|
||||
const iframe = document.getElementById("exportIFrame");
|
||||
const form = iframe.querySelector("form");
|
||||
form.action = url;
|
||||
form.target = "exportIFrame";
|
||||
(iframe.querySelector("#authorizationInput") as HTMLInputElement).value = qController.getAuthorizationHeaderValue();
|
||||
form.submit();
|
||||
*/
|
||||
|
||||
///////////////////////////////////////////
|
||||
// Hide the export menu after the export //
|
||||
///////////////////////////////////////////
|
||||
hideMenu?.();
|
||||
}}
|
||||
>
|
||||
Export
|
||||
{` ${format.toUpperCase()}`}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -1,736 +0,0 @@
|
||||
/*
|
||||
* 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 {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";
|
||||
import Button from "@mui/material/Button";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import List from "@mui/material/List/List";
|
||||
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";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
FieldListMenu.defaultProps = {
|
||||
showTableHeaderEvenIfNoExposedJoins: false,
|
||||
isModeSelectOne: false,
|
||||
isModeToggle: false,
|
||||
};
|
||||
|
||||
interface TableWithFields
|
||||
{
|
||||
table?: QTableMetaData;
|
||||
fields: QFieldMetaData[];
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** 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
|
||||
{
|
||||
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 [lastMouseOverXY, setLastMouseOverXY] = useState({x: 0, y: 0});
|
||||
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0)
|
||||
|
||||
//////////////////
|
||||
// check usages //
|
||||
//////////////////
|
||||
if(isModeSelectOne)
|
||||
{
|
||||
if(!handleSelectedField)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeSelectOne=true, then a callback for handleSelectedField must be provided.");
|
||||
}
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
{
|
||||
if(!toggleStates)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeToggle=true, then a model for toggleStates must be provided.");
|
||||
}
|
||||
if(!handleToggleField)
|
||||
{
|
||||
throw("In FieldListMenu, if isModeToggle=true, then a callback for handleToggleField must be provided.");
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// init some stuff //
|
||||
/////////////////////
|
||||
if (fieldsByTable.length == 0)
|
||||
{
|
||||
collapsedTables[tableMetaData.name] = false;
|
||||
|
||||
if (tableMetaData.exposedJoins?.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++)
|
||||
{
|
||||
const joinTable = tableMetaData.exposedJoins[i].joinTable;
|
||||
fieldsByTable.push({table: joinTable, fields: getTableFieldsAsAlphabeticalArray(joinTable)});
|
||||
|
||||
collapsedTables[joinTable.name] = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////
|
||||
// no exposed joins - just the table (w/o its meta-data) //
|
||||
///////////////////////////////////////////////////////////
|
||||
fieldsByTable.push({fields: getTableFieldsAsAlphabeticalArray(tableMetaData)});
|
||||
}
|
||||
|
||||
setFieldsByTable(fieldsByTable);
|
||||
setCollapsedTables(collapsedTables);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getTableFieldsAsAlphabeticalArray(table: QTableMetaData): QFieldMetaData[]
|
||||
{
|
||||
const fields: QFieldMetaData[] = [];
|
||||
table.fields.forEach(field =>
|
||||
{
|
||||
let fullFieldName = field.name;
|
||||
if(table.name != tableMetaData.name)
|
||||
{
|
||||
fullFieldName = `${table.name}.${field.name}`;
|
||||
}
|
||||
|
||||
if(fieldNamesToHide && fieldNamesToHide.indexOf(fullFieldName) > -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
fields.push(field)
|
||||
});
|
||||
fields.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return (fields);
|
||||
}
|
||||
|
||||
const fieldsByTableToShow: TableWithFields[] = [];
|
||||
let maxFieldIndex = 0;
|
||||
fieldsByTable.forEach((tableWithFields) =>
|
||||
{
|
||||
let fieldsToShowForThisTable = tableWithFields.fields.filter(doesFieldMatchSearchText);
|
||||
if (fieldsToShowForThisTable.length > 0)
|
||||
{
|
||||
fieldsByTableToShow.push({table: tableWithFields.table, fields: fieldsToShowForThisTable});
|
||||
maxFieldIndex += fieldsToShowForThisTable.length;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getShownFieldAndTableByIndex(targetIndex: number): {field: QFieldMetaData, table: QTableMetaData}
|
||||
{
|
||||
let index = -1;
|
||||
for (let i = 0; i < fieldsByTableToShow.length; i++)
|
||||
{
|
||||
const tableWithField = fieldsByTableToShow[i];
|
||||
for (let j = 0; j < tableWithField.fields.length; j++)
|
||||
{
|
||||
index++;
|
||||
|
||||
if(index == targetIndex)
|
||||
{
|
||||
return {field: tableWithField.fields[j], table: tableWithField.table}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for keys presses
|
||||
*******************************************************************************/
|
||||
function keyDown(event: any)
|
||||
{
|
||||
// console.log(`Event key: ${event.key}`);
|
||||
setTimeout(() => document.getElementById(`field-list-dropdown-${idPrefix}-textField`).focus());
|
||||
|
||||
if(isModeSelectOne && event.key == "Enter" && focusedIndex != null)
|
||||
{
|
||||
setTimeout(() =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
const {field, table} = getShownFieldAndTableByIndex(focusedIndex);
|
||||
if (field)
|
||||
{
|
||||
handleSelectedField(field, table ?? tableMetaData);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const keyOffsetMap: { [key: string]: number } = {
|
||||
"End": 10000,
|
||||
"Home": -10000,
|
||||
"ArrowDown": 1,
|
||||
"ArrowUp": -1,
|
||||
"PageDown": 5,
|
||||
"PageUp": -5,
|
||||
};
|
||||
|
||||
const offset = keyOffsetMap[event.key];
|
||||
if (offset)
|
||||
{
|
||||
event.stopPropagation();
|
||||
setTimeOfLastArrow(new Date().getTime());
|
||||
|
||||
if (isModeSelectOne)
|
||||
{
|
||||
let startIndex = focusedIndex;
|
||||
if (offset > 0)
|
||||
{
|
||||
/////////////////
|
||||
// a down move //
|
||||
/////////////////
|
||||
if(startIndex == null)
|
||||
{
|
||||
startIndex = -1;
|
||||
}
|
||||
|
||||
let goalIndex = startIndex + offset;
|
||||
if(goalIndex > maxFieldIndex - 1)
|
||||
{
|
||||
goalIndex = maxFieldIndex - 1;
|
||||
}
|
||||
|
||||
doSetFocusedIndex(goalIndex, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////
|
||||
// an up move //
|
||||
////////////////
|
||||
let goalIndex = startIndex + offset;
|
||||
if(goalIndex < 0)
|
||||
{
|
||||
goalIndex = 0;
|
||||
}
|
||||
|
||||
doSetFocusedIndex(goalIndex, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function doSetFocusedIndex(i: number, tryToScrollIntoView: boolean): void
|
||||
{
|
||||
if (isModeSelectOne)
|
||||
{
|
||||
setFocusedIndex(i);
|
||||
console.log(`Setting index to ${i}`);
|
||||
|
||||
if (tryToScrollIntoView)
|
||||
{
|
||||
const element = document.getElementById(`field-list-dropdown-${idPrefix}-${i}`);
|
||||
element?.scrollIntoView({block: "center"});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function setFocusedField(field: QFieldMetaData, table: QTableMetaData, tryToScrollIntoView: boolean)
|
||||
{
|
||||
let index = -1;
|
||||
for (let i = 0; i < fieldsByTableToShow.length; i++)
|
||||
{
|
||||
const tableWithField = fieldsByTableToShow[i];
|
||||
for (let j = 0; j < tableWithField.fields.length; j++)
|
||||
{
|
||||
const loopField = tableWithField.fields[j];
|
||||
index++;
|
||||
|
||||
const tableMatches = (table == null || table.name == tableWithField.table.name);
|
||||
if (tableMatches && field.name == loopField.name)
|
||||
{
|
||||
doSetFocusedIndex(index, tryToScrollIntoView);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for mouse-over the menu
|
||||
*******************************************************************************/
|
||||
function handleMouseOver(event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLDivElement> | React.MouseEvent<HTMLLIElement>, field: QFieldMetaData, table: QTableMetaData)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// so we're trying to fix the case where, if you put your mouse over an element, but then press up/down arrows, //
|
||||
// where the mouse will become over a different element after the scroll, and the focus will follow the mouse instead of keyboard. //
|
||||
// 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)
|
||||
{
|
||||
// console.log("mouse didn't move, so, doesn't count");
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().getTime();
|
||||
// console.log(`Compare now [${now}] to last arrow [${timeOfLastArrow}] (diff: [${now - timeOfLastArrow}])`);
|
||||
if(now < timeOfLastArrow + 300)
|
||||
{
|
||||
// console.log("An arrow event happened less than 300 mills ago, so doesn't count.");
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("yay, mouse over...");
|
||||
setFocusedField(field, table, false);
|
||||
setLastMouseOverXY({x: event.clientX, y: event.clientY});
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for text input changes
|
||||
*******************************************************************************/
|
||||
function updateSearch(event: React.ChangeEvent<HTMLInputElement>)
|
||||
{
|
||||
setSearchText(event?.target?.value ?? "");
|
||||
doSetFocusedIndex(0, true);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function doesFieldMatchSearchText(field: QFieldMetaData): boolean
|
||||
{
|
||||
if (searchText == "")
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
const columnLabelMinusTable = field.label.replace(/.*: /, "");
|
||||
if (columnLabelMinusTable.toLowerCase().startsWith(searchText.toLowerCase()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
// try to match word-boundary followed by the filter text //
|
||||
// e.g., "name" would match "First Name" or "Last Name" //
|
||||
////////////////////////////////////////////////////////////
|
||||
const re = new RegExp("\\b" + searchText.toLowerCase());
|
||||
if (columnLabelMinusTable.toLowerCase().match(re))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// in case text is an invalid regex... well, at least do a starts-with match... //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (columnLabelMinusTable.toLowerCase().startsWith(searchText.toLowerCase()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
const tableLabel = field.label.replace(/:.*/, "");
|
||||
if (tableLabel)
|
||||
{
|
||||
try
|
||||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
// try to match word-boundary followed by the filter text //
|
||||
// e.g., "name" would match "First Name" or "Last Name" //
|
||||
////////////////////////////////////////////////////////////
|
||||
const re = new RegExp("\\b" + searchText.toLowerCase());
|
||||
if (tableLabel.toLowerCase().match(re))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// in case text is an invalid regex... well, at least do a starts-with match... //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (tableLabel.toLowerCase().startsWith(searchText.toLowerCase()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function openMenu(event: any)
|
||||
{
|
||||
setFocusedIndex(null);
|
||||
setMenuAnchorElement(event.currentTarget);
|
||||
setTimeout(() =>
|
||||
{
|
||||
document.getElementById(`field-list-dropdown-${idPrefix}-textField`).focus();
|
||||
doSetFocusedIndex(0, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function closeMenu()
|
||||
{
|
||||
setMenuAnchorElement(null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for toggling a field in toggle mode
|
||||
*******************************************************************************/
|
||||
function handleFieldToggle(event: React.ChangeEvent<HTMLInputElement>, field: QFieldMetaData, table: QTableMetaData)
|
||||
{
|
||||
event.stopPropagation();
|
||||
handleToggleField(field, table, event.target.checked);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for toggling a table in toggle mode
|
||||
*******************************************************************************/
|
||||
function handleTableToggle(event: React.ChangeEvent<HTMLInputElement>, table: QTableMetaData)
|
||||
{
|
||||
event.stopPropagation();
|
||||
|
||||
const fieldsList = [...table.fields.values()];
|
||||
for (let i = 0; i < fieldsList.length; i++)
|
||||
{
|
||||
const field = fieldsList[i];
|
||||
if(doesFieldMatchSearchText(field))
|
||||
{
|
||||
handleToggleField(field, table, event.target.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// compute the table-level toggle state & count values //
|
||||
/////////////////////////////////////////////////////////
|
||||
const tableToggleStates: {[tableName: string]: boolean} = {};
|
||||
const tableToggleCounts: {[tableName: string]: number} = {};
|
||||
|
||||
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++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
const {allOn, count} = getTableToggleState(join.joinTable, false);
|
||||
tableToggleStates[join.joinTable.name] = allOn;
|
||||
tableToggleCounts[join.joinTable.name] = count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getTableToggleState(table: QTableMetaData, isMainTable: boolean): {allOn: boolean, count: number}
|
||||
{
|
||||
const fieldsList = [...table.fields.values()];
|
||||
let allOn = true;
|
||||
let count = 0;
|
||||
for (let i = 0; i < fieldsList.length; i++)
|
||||
{
|
||||
const field = fieldsList[i];
|
||||
const name = isMainTable ? field.name : `${table.name}.${field.name}`;
|
||||
if(!toggleStates[name])
|
||||
{
|
||||
allOn = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return ({allOn: allOn, count: count});
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function toggleCollapsedTable(tableName: string)
|
||||
{
|
||||
collapsedTables[tableName] = !collapsedTables[tableName]
|
||||
setCollapsedTables(Object.assign({}, collapsedTables));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function doHandleAdornmentClick(field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>)
|
||||
{
|
||||
console.log("In doHandleAdornmentClick");
|
||||
closeMenu();
|
||||
handleAdornmentClick(field, table, event);
|
||||
}
|
||||
|
||||
|
||||
let index = -1;
|
||||
const textFieldId = `field-list-dropdown-${idPrefix}-textField`;
|
||||
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) //
|
||||
// then we increment i by 2 for the next table (so the next header goes above the previous header) //
|
||||
// this fixes a thing where, if one table's name wrapped to 2 lines, then when the next table below it would //
|
||||
// come up, if it was only 1 line, then the second line from the previous one would bleed through. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let zIndex = 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={openMenu} {...buttonProps}>
|
||||
{buttonChildren}
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={menuAnchorElement}
|
||||
anchorOrigin={{vertical: "bottom", horizontal: "left"}}
|
||||
transformOrigin={{vertical: "top", horizontal: "left"}}
|
||||
open={menuAnchorElement != null}
|
||||
onClose={closeMenu}
|
||||
onKeyDown={keyDown} // this is added here so arrow-key-up/down events don't make the whole menu become "focused" (blue outline). it works.
|
||||
keepMounted
|
||||
>
|
||||
<Box width={isModeToggle ? "305px" : "265px"} borderRadius={2} className={`fieldListMenuBody fieldListMenuBody-${idPrefix}`}>
|
||||
{
|
||||
heading &&
|
||||
<Box px={1} py={0.5} fontWeight={"700"}>
|
||||
{heading}
|
||||
</Box>
|
||||
}
|
||||
<Box p={1} pt={0.5}>
|
||||
<TextField id={textFieldId} variant="outlined" placeholder={placeholder ?? "Search Fields"} fullWidth value={searchText} onChange={updateSearch} onKeyDown={keyDown} inputProps={{sx: {pr: "2rem"}}} />
|
||||
{
|
||||
searchText != "" && <IconButton sx={{position: "absolute", right: "0.5rem", top: "0.5rem"}} onClick={() =>
|
||||
{
|
||||
updateSearch(null);
|
||||
document.getElementById(textFieldId).focus();
|
||||
}}><Icon fontSize="small">close</Icon></IconButton>
|
||||
}
|
||||
</Box>
|
||||
<Box maxHeight={"445px"} overflow="auto" mr={"-0.5rem"} sx={{scrollbarGutter: "stable"}}>
|
||||
<List sx={{px: "0.5rem", cursor: "default"}}>
|
||||
{
|
||||
fieldsByTableToShow.map((tableWithFields) =>
|
||||
{
|
||||
let headerContents = null;
|
||||
const headerTable = tableWithFields.table || tableMetaData;
|
||||
if(tableWithFields.table || showTableHeaderEvenIfNoExposedJoins)
|
||||
{
|
||||
headerContents = (<b>{headerTable.label} Fields</b>);
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
{
|
||||
headerContents = (<FormControlLabel
|
||||
sx={{display: "flex", alignItems: "flex-start", "& .MuiFormControlLabel-label": {lineHeight: "1.4", fontWeight: "500 !important"}}}
|
||||
control={<Switch
|
||||
size="small"
|
||||
sx={{top: "1px"}}
|
||||
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>} />)
|
||||
}
|
||||
|
||||
if(isModeToggle)
|
||||
{
|
||||
headerContents = (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => toggleCollapsedTable(headerTable.name)}
|
||||
sx={{justifyContent: "flex-start", fontSize: "0.875rem", pt: 0.5, pb: 0, mr: "0.25rem"}}
|
||||
disableRipple={true}
|
||||
>
|
||||
<Icon sx={{fontSize: "1.5rem !important", position: "relative", top: "2px"}}>{collapsedTables[headerTable.name] ? "expand_less" : "expand_more"}</Icon>
|
||||
</IconButton>
|
||||
{headerContents}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
let marginLeft = "unset";
|
||||
if(isModeToggle)
|
||||
{
|
||||
marginLeft = "-1rem";
|
||||
}
|
||||
|
||||
zIndex += 2;
|
||||
|
||||
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>}
|
||||
{
|
||||
tableWithFields.fields.map((field) =>
|
||||
{
|
||||
index++;
|
||||
const key = `${tableWithFields.table?.name}-${field.name}`
|
||||
|
||||
if(collapsedTables[headerTable.name])
|
||||
{
|
||||
return (<React.Fragment key={key} />);
|
||||
}
|
||||
|
||||
let style = {};
|
||||
if (index == focusedIndex)
|
||||
{
|
||||
style = {backgroundColor: "#EFEFEF"};
|
||||
}
|
||||
|
||||
const onClick: ListItemProps = {};
|
||||
if (isModeSelectOne)
|
||||
{
|
||||
onClick.onClick = () =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
label = <Box width="100%" display="inline-flex" justifyContent="space-between">
|
||||
{label}
|
||||
<Box onClick={(event) => doHandleAdornmentClick(field, tableWithFields.table, event)}>
|
||||
{fieldEndAdornment}
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
let contents = <>{label}</>;
|
||||
let paddingLeft = "0.5rem";
|
||||
|
||||
if (isModeToggle)
|
||||
{
|
||||
contents = (<FormControlLabel
|
||||
sx={{display: "flex", alignItems: "flex-start", "& .MuiFormControlLabel-label": {lineHeight: "1.4", color: "#606060", fontWeight: "500 !important"}}}
|
||||
control={<Switch
|
||||
size="small"
|
||||
sx={{top: "-3px"}}
|
||||
checked={toggleStates[fullFieldName]}
|
||||
onChange={(event) => handleFieldToggle(event, field, tableWithFields.table)}
|
||||
/>}
|
||||
label={label} />);
|
||||
paddingLeft = "2.5rem";
|
||||
}
|
||||
|
||||
return <ListItem
|
||||
key={key}
|
||||
id={`field-list-dropdown-${idPrefix}-${index}`}
|
||||
sx={{color: "#757575", p: 1, borderRadius: ".5rem", padding: listItemPadding, pl: paddingLeft, scrollMarginTop: "3rem", zIndex: zIndex, background: "#FFFFFF", ...style}}
|
||||
onMouseOver={(event) => handleMouseOver(event, field, tableWithFields.table)}
|
||||
{...onClick}
|
||||
>{contents}</ListItem>;
|
||||
})
|
||||
}
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
index == -1 && <ListItem sx={{p: "0.5rem"}}><i>No fields found.</i></ListItem>
|
||||
}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
@ -24,7 +24,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import FormControl from "@mui/material/FormControl/FormControl";
|
||||
import Icon from "@mui/material/Icon/Icon";
|
||||
@ -34,7 +34,6 @@ import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import React, {ReactNode, SyntheticEvent, useState} from "react";
|
||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
||||
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
|
||||
@ -53,27 +52,6 @@ export enum ValueMode
|
||||
PVS_MULTI = "PVS_MULTI",
|
||||
}
|
||||
|
||||
export const getValueModeRequiredCount = (valueMode: ValueMode): number =>
|
||||
{
|
||||
switch (valueMode)
|
||||
{
|
||||
case ValueMode.NONE:
|
||||
return (0);
|
||||
case ValueMode.SINGLE:
|
||||
case ValueMode.SINGLE_DATE:
|
||||
case ValueMode.SINGLE_DATE_TIME:
|
||||
case ValueMode.PVS_SINGLE:
|
||||
return (1);
|
||||
case ValueMode.DOUBLE:
|
||||
case ValueMode.DOUBLE_DATE:
|
||||
case ValueMode.DOUBLE_DATE_TIME:
|
||||
return (2);
|
||||
case ValueMode.MULTI:
|
||||
case ValueMode.PVS_MULTI:
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
|
||||
export interface OperatorOption
|
||||
{
|
||||
label: string;
|
||||
@ -199,71 +177,16 @@ interface FilterCriteriaRowProps
|
||||
updateBooleanOperator: (newValue: string) => void;
|
||||
}
|
||||
|
||||
FilterCriteriaRow.defaultProps =
|
||||
{
|
||||
};
|
||||
FilterCriteriaRow.defaultProps = {};
|
||||
|
||||
export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValue?: OperatorOption): {criteriaIsValid: boolean, criteriaStatusTooltip: string}
|
||||
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean)
|
||||
{
|
||||
let criteriaIsValid = true;
|
||||
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
|
||||
|
||||
function isNotSet(value: any)
|
||||
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||
for (let i = 0; i < sortedFields.length; i++)
|
||||
{
|
||||
return (value === null || value == undefined || String(value).trim() === "");
|
||||
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
|
||||
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
|
||||
}
|
||||
|
||||
if(!criteria)
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "This condition is not defined.";
|
||||
return {criteriaIsValid, criteriaStatusTooltip};
|
||||
}
|
||||
|
||||
if (!criteria.fieldName)
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
|
||||
}
|
||||
else if (!criteria.operator)
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (criteria.operator == QCriteriaOperator.IS_BLANK || criteria.operator == QCriteriaOperator.IS_NOT_BLANK)
|
||||
{
|
||||
//////////////////////////////////
|
||||
// don't need to look at values //
|
||||
//////////////////////////////////
|
||||
}
|
||||
else if (criteria.operator == QCriteriaOperator.BETWEEN || criteria.operator == QCriteriaOperator.NOT_BETWEEN)
|
||||
{
|
||||
if (criteria.values.length < 2 || isNotSet(criteria.values[0]) || isNotSet(criteria.values[1]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter two values to complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
else if (criteria.operator == QCriteriaOperator.IN || criteria.operator == QCriteriaOperator.NOT_IN)
|
||||
{
|
||||
if (criteria.values.length < 1 || isNotSet(criteria.values[0]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!criteria.values || isNotSet(criteria.values[0]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
}
|
||||
return {criteriaIsValid, criteriaStatusTooltip};
|
||||
}
|
||||
|
||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
|
||||
@ -272,6 +195,27 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
||||
const [operatorInputValue, setOperatorInputValue] = useState("");
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// set up the array of options for the fields Autocomplete //
|
||||
// also, a groupBy function, in case there are exposed joins //
|
||||
///////////////////////////////////////////////////////////////
|
||||
const fieldOptions: any[] = [];
|
||||
makeFieldOptionsForTable(tableMetaData, fieldOptions, false);
|
||||
let fieldsGroupBy = null;
|
||||
|
||||
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const exposedJoin = tableMetaData.exposedJoins[i];
|
||||
if (metaData.tables.has(exposedJoin.joinTable.name))
|
||||
{
|
||||
fieldsGroupBy = (option: any) => `${option.table.label} fields`;
|
||||
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// set up array of options for operator dropdown //
|
||||
// only call the function to do it if we have a field set //
|
||||
@ -388,24 +332,6 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
{
|
||||
criteria.values = newValue.implicitValues;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we've seen cases where switching operators can sometimes put a null in as the first value... //
|
||||
// that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null)
|
||||
{
|
||||
criteria.values = [];
|
||||
}
|
||||
|
||||
if(newValue.valueMode && !newValue.implicitValues)
|
||||
{
|
||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||
{
|
||||
criteria.values.splice(requiredValueCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -457,19 +383,111 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
return (false);
|
||||
};
|
||||
|
||||
function isFieldOptionEqual(option: any, value: any)
|
||||
{
|
||||
return option.fieldName === value.fieldName;
|
||||
}
|
||||
|
||||
function getFieldOptionLabel(option: any)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - we're using renderFieldOption below for the actual select-box options, which //
|
||||
// are always jut field label (as they are under groupings that show their table name) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(option && option.field && option.table)
|
||||
{
|
||||
if(option.table.name == tableMetaData.name)
|
||||
{
|
||||
return (option.field.label);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (option.table.label + ": " + option.field.label);
|
||||
}
|
||||
}
|
||||
|
||||
return ("");
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for options, we only want the field label (contrast with what we show in the input box, //
|
||||
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
|
||||
{
|
||||
let label = ""
|
||||
if(option && option.field)
|
||||
{
|
||||
label = (option.field.label);
|
||||
}
|
||||
|
||||
return (<li {...props}>{label}</li>);
|
||||
}
|
||||
|
||||
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
|
||||
{
|
||||
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
|
||||
}
|
||||
|
||||
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
||||
let criteriaIsValid = true;
|
||||
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
|
||||
|
||||
const tooltipEnterDelay = 750;
|
||||
function isNotSet(value: any)
|
||||
{
|
||||
return (value === null || value == undefined || String(value).trim() === "");
|
||||
}
|
||||
|
||||
if(!criteria.fieldName)
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
|
||||
}
|
||||
else if(!criteria.operator)
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
|
||||
}
|
||||
else
|
||||
{
|
||||
if(operatorSelectedValue)
|
||||
{
|
||||
if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues)
|
||||
{
|
||||
//////////////////////////////////
|
||||
// don't need to look at values //
|
||||
//////////////////////////////////
|
||||
}
|
||||
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE || operatorSelectedValue.valueMode == ValueMode.DOUBLE_DATE_TIME)
|
||||
{
|
||||
if(criteria.values.length < 2 || isNotSet(criteria.values[0]) || isNotSet(criteria.values[1]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter two values to complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
else if(operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI)
|
||||
{
|
||||
if(criteria.values.length < 1 || isNotSet(criteria.values[0]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!criteria.values || isNotSet(criteria.values[0]))
|
||||
{
|
||||
criteriaIsValid = false;
|
||||
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="filterCriteriaRow" pt={0.5} display="flex" alignItems="flex-end" pr={0.5}>
|
||||
<Box className="filterCriteriaRow" pt={0.5} display="flex" alignItems="flex-end">
|
||||
<Box display="inline-block">
|
||||
<Tooltip title="Remove this condition from your filter" enterDelay={tooltipEnterDelay} placement="left">
|
||||
<Tooltip title="Remove this condition from your filter" enterDelay={750} placement="left">
|
||||
<IconButton onClick={removeCriteria}><Icon fontSize="small">close</Icon></IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@ -484,10 +502,24 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
: <span />}
|
||||
</Box>
|
||||
<Box display="inline-block" width={250} className="fieldColumn">
|
||||
<FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange} />
|
||||
<Autocomplete
|
||||
id={`field-${id}`}
|
||||
renderInput={(params) => (<TextField {...params} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
// @ts-ignore
|
||||
defaultValue={defaultFieldValue}
|
||||
options={fieldOptions}
|
||||
onChange={handleFieldChange}
|
||||
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
|
||||
groupBy={fieldsGroupBy}
|
||||
getOptionLabel={(option) => getFieldOptionLabel(option)}
|
||||
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
|
||||
autoSelect={true}
|
||||
autoHighlight={true}
|
||||
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="inline-block" width={200} className="operatorColumn">
|
||||
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={tooltipEnterDelay}>
|
||||
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={750}>
|
||||
<Autocomplete
|
||||
id={"criteriaOperator"}
|
||||
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
@ -514,8 +546,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="inline-block">
|
||||
<Tooltip title={criteriaStatusTooltip} enterDelay={tooltipEnterDelay} placement="bottom">
|
||||
<Box display="inline-block" pl={0.5} pr={1}>
|
||||
<Tooltip title={criteriaStatusTooltip} enterDelay={750} placement="right">
|
||||
{
|
||||
criteriaIsValid
|
||||
? <Icon color="success">check</Icon>
|
||||
|
@ -44,13 +44,9 @@ interface Props
|
||||
field: QFieldMetaData;
|
||||
table: QTableMetaData;
|
||||
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
|
||||
initiallyOpenMultiValuePvs?: boolean
|
||||
}
|
||||
|
||||
FilterCriteriaRowValues.defaultProps =
|
||||
{
|
||||
initiallyOpenMultiValuePvs: false
|
||||
};
|
||||
FilterCriteriaRowValues.defaultProps = {};
|
||||
|
||||
export const getTypeForTextField = (field: QFieldMetaData): string =>
|
||||
{
|
||||
@ -114,17 +110,16 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
InputLabelProps={inputLabelProps}
|
||||
InputProps={inputProps}
|
||||
fullWidth
|
||||
autoFocus={true}
|
||||
/>;
|
||||
};
|
||||
|
||||
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs}: Props): JSX.Element
|
||||
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element
|
||||
{
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
if (!operatorOption)
|
||||
{
|
||||
return null;
|
||||
return <br />;
|
||||
}
|
||||
|
||||
function saveNewPasterValues(newValues: any[])
|
||||
@ -153,7 +148,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
switch (operatorOption.valueMode)
|
||||
{
|
||||
case ValueMode.NONE:
|
||||
return null;
|
||||
return <br />;
|
||||
case ValueMode.SINGLE:
|
||||
return makeTextField(field, criteria, valueChangeHandler);
|
||||
case ValueMode.SINGLE_DATE:
|
||||
@ -220,7 +215,6 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||
variant="standard"
|
||||
/>
|
||||
</Box>;
|
||||
case ValueMode.PVS_MULTI:
|
||||
@ -246,10 +240,8 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
isMultiple
|
||||
fieldLabel="Values"
|
||||
initialValues={initialValues}
|
||||
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||
variant="standard"
|
||||
/>
|
||||
</Box>;
|
||||
}
|
||||
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* 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 {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||
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 Divider from "@mui/material/Divider";
|
||||
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 React, {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||
|
||||
interface QueryScreenActionMenuProps
|
||||
{
|
||||
metaData: QInstance;
|
||||
tableMetaData: QTableMetaData;
|
||||
tableProcesses: QProcessMetaData[];
|
||||
bulkLoadClicked: () => void;
|
||||
bulkEditClicked: () => void;
|
||||
bulkDeleteClicked: () => void;
|
||||
processClicked: (process: QProcessMetaData) => void;
|
||||
}
|
||||
|
||||
QueryScreenActionMenu.defaultProps = {
|
||||
};
|
||||
|
||||
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
|
||||
{
|
||||
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[] = [];
|
||||
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>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission)
|
||||
{
|
||||
menuItems.push(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||
}
|
||||
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
tableProcesses.sort((a, b) => a.label.localeCompare(b.label));
|
||||
tableProcesses.map((process) =>
|
||||
{
|
||||
menuItems.push(<MenuItem key={process.name} onClick={() => runSomething(() => processClicked(process))}><ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>{process.label}</MenuItem>);
|
||||
});
|
||||
|
||||
if (menuItems.length === 0)
|
||||
{
|
||||
menuItems.push(<MenuItem key="notAvaialableNow" disabled><ListItemIcon><Icon>block</Icon></ListItemIcon><i>No actions available</i></MenuItem>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<QActionsMenuButton isOpen={anchorElement} onClickHandler={openActionsMenu} />
|
||||
<Menu
|
||||
anchorEl={anchorElement}
|
||||
anchorOrigin={{vertical: "bottom", horizontal: "right",}}
|
||||
transformOrigin={{vertical: "top", horizontal: "right",}}
|
||||
open={anchorElement != null}
|
||||
onClose={closeActionsMenu}
|
||||
keepMounted
|
||||
>
|
||||
{menuItems}
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,511 +0,0 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import {Tooltip} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {SyntheticEvent, useContext, useState} from "react";
|
||||
import QContext from "QContext";
|
||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||
import {getDefaultCriteriaValue, getOperatorOptions, getValueModeRequiredCount, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
||||
import XIcon from "qqq/components/query/XIcon";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
|
||||
export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
|
||||
|
||||
interface QuickFilterProps
|
||||
{
|
||||
tableMetaData: QTableMetaData;
|
||||
fullFieldName: string;
|
||||
fieldMetaData: QFieldMetaData;
|
||||
criteriaParam: CriteriaParamType;
|
||||
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean, doRemoveCriteria: boolean) => void;
|
||||
defaultOperator?: QCriteriaOperator;
|
||||
handleRemoveQuickFilterField?: (fieldName: string) => void;
|
||||
}
|
||||
|
||||
QuickFilter.defaultProps =
|
||||
{
|
||||
defaultOperator: QCriteriaOperator.EQUALS,
|
||||
handleRemoveQuickFilterField: null
|
||||
};
|
||||
|
||||
let seedId = new Date().getTime() % 173237;
|
||||
|
||||
export const quickFilterButtonStyles = {
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 600,
|
||||
color: "#757575",
|
||||
textTransform: "none",
|
||||
borderRadius: "2rem",
|
||||
border: "1px solid #757575",
|
||||
minWidth: "3.5rem",
|
||||
minHeight: "auto",
|
||||
padding: "0.375rem 0.625rem", whiteSpace: "nowrap",
|
||||
marginBottom: "0.5rem"
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Test if a CriteriaParamType represents an actual query criteria - or, if it's
|
||||
** null or the "tooComplex" placeholder.
|
||||
*******************************************************************************/
|
||||
const criteriaParamIsCriteria = (param: CriteriaParamType): boolean =>
|
||||
{
|
||||
return (param != null && param != "tooComplex");
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** Test of an OperatorOption equals a query Criteria - that is - that the
|
||||
** operators within them are equal - AND - if the OperatorOption has implicit
|
||||
** values (e.g., the booleans), then those options equal the criteria's options.
|
||||
*******************************************************************************/
|
||||
const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteria: QFilterCriteriaWithId): boolean =>
|
||||
{
|
||||
if(operatorOption.value == criteria.operator)
|
||||
{
|
||||
if(operatorOption.implicitValues)
|
||||
{
|
||||
if(JSON.stringify(operatorOption.implicitValues) == JSON.stringify(criteria.values))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the object to use as the selected OperatorOption (e.g., value for that
|
||||
** 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 =>
|
||||
{
|
||||
if(criteria)
|
||||
{
|
||||
const filteredOptions = operatorOptions.filter(o => doesOperatorOptionEqualCriteria(o, criteria));
|
||||
if(filteredOptions.length > 0)
|
||||
{
|
||||
return (filteredOptions[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredOptions = operatorOptions.filter(o => o.value == defaultOperator);
|
||||
if(filteredOptions.length > 0)
|
||||
{
|
||||
return (filteredOptions[0]);
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to render a QuickFilter - that is - a button, with a Menu under it,
|
||||
** with Operator and Value controls.
|
||||
*******************************************************************************/
|
||||
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField}: QuickFilterProps): JSX.Element
|
||||
{
|
||||
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
||||
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
|
||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? criteriaParam as QFilterCriteriaWithId : null);
|
||||
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
||||
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
||||
|
||||
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
||||
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function handleMouseOverElement()
|
||||
{
|
||||
setIsMouseOver(true);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function handleMouseOutElement()
|
||||
{
|
||||
setIsMouseOver(false);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// handle a change to the criteria from outside this component (e.g., the prop isn't the same as the state) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (criteriaParamIsCriteria(criteriaParam) && JSON.stringify(criteriaParam) !== JSON.stringify(criteria))
|
||||
{
|
||||
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
||||
setCriteria(newCriteria);
|
||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||
setOperatorSelectedValue(operatorOption);
|
||||
setOperatorInputValue(operatorOption.label);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Test if we need to construct a new criteria object
|
||||
*******************************************************************************/
|
||||
const criteriaNeedsReset = (): boolean =>
|
||||
{
|
||||
if(criteria != null && criteriaParam == null)
|
||||
{
|
||||
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||
if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a new criteria object - resetting the values tied to the oprator
|
||||
** autocomplete at the same time.
|
||||
*******************************************************************************/
|
||||
const makeNewCriteria = (): QFilterCriteria =>
|
||||
{
|
||||
const operatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||
const criteria = new QFilterCriteriaWithId(fullFieldName, operatorOption?.value, getDefaultCriteriaValue());
|
||||
criteria.id = id;
|
||||
setOperatorSelectedValue(operatorOption);
|
||||
setOperatorInputValue(operatorOption?.label);
|
||||
setCriteria(criteria);
|
||||
return(criteria);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler to open the menu in response to the button being clicked.
|
||||
*******************************************************************************/
|
||||
const handleOpenMenu = (event: any) =>
|
||||
{
|
||||
setIsOpen(!isOpen);
|
||||
setAnchorEl(event.currentTarget);
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
const element = document.getElementById("value-" + criteria.id);
|
||||
element?.focus();
|
||||
})
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** handler for the Menu when being closed
|
||||
*******************************************************************************/
|
||||
const closeMenu = () =>
|
||||
{
|
||||
setIsOpen(false);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for operator Autocomplete having its value changed
|
||||
*******************************************************************************/
|
||||
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
criteria.operator = newValue ? newValue.value : null;
|
||||
|
||||
if (newValue)
|
||||
{
|
||||
setOperatorSelectedValue(newValue);
|
||||
setOperatorInputValue(newValue.label);
|
||||
|
||||
if (newValue.implicitValues)
|
||||
{
|
||||
criteria.values = newValue.implicitValues;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we've seen cases where switching operators can sometimes put a null in as the first value... //
|
||||
// that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null)
|
||||
{
|
||||
criteria.values = [];
|
||||
}
|
||||
|
||||
if(newValue.valueMode && !newValue.implicitValues)
|
||||
{
|
||||
const requiredValueCount = getValueModeRequiredCount(newValue.valueMode);
|
||||
if(requiredValueCount != null && criteria.values.length > requiredValueCount)
|
||||
{
|
||||
criteria.values.splice(requiredValueCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setOperatorSelectedValue(null);
|
||||
setOperatorInputValue("");
|
||||
}
|
||||
|
||||
updateCriteria(criteria, false, false);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** implementation of isOptionEqualToValue for Autocomplete - compares both the
|
||||
** value (e.g., what operator it is) and the implicitValues within the option
|
||||
*******************************************************************************/
|
||||
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
|
||||
{
|
||||
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for the value field (of all types), when it changes
|
||||
*******************************************************************************/
|
||||
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
|
||||
{
|
||||
// @ts-ignore
|
||||
const value = newValue !== undefined ? newValue : event ? event.target.value : null;
|
||||
|
||||
if (!criteria.values)
|
||||
{
|
||||
criteria.values = [];
|
||||
}
|
||||
|
||||
if (valueIndex == "all")
|
||||
{
|
||||
criteria.values = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
criteria.values[valueIndex] = value;
|
||||
}
|
||||
|
||||
updateCriteria(criteria, true, false);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** a noop event handler, e.g., for a too-complex
|
||||
*******************************************************************************/
|
||||
const noop = () =>
|
||||
{
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler that responds to 'x' button that removes the criteria from the
|
||||
** quick-filter, resetting it to a new filter.
|
||||
*******************************************************************************/
|
||||
const resetCriteria = (e: React.MouseEvent<HTMLSpanElement>) =>
|
||||
{
|
||||
if(criteriaIsValid)
|
||||
{
|
||||
e.stopPropagation();
|
||||
const newCriteria = makeNewCriteria();
|
||||
updateCriteria(newCriteria, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for clicking the (x) icon that turns off this quick filter field.
|
||||
** hands off control to the function that was passed in (e.g., from RecordQueryOrig).
|
||||
*******************************************************************************/
|
||||
const handleTurningOffQuickFilterField = () =>
|
||||
{
|
||||
closeMenu()
|
||||
if(handleRemoveQuickFilterField)
|
||||
{
|
||||
handleRemoveQuickFilterField(criteria?.fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if no field was input (e.g., record-query is still loading), return null early //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!fieldMetaData)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there should be a selected value in the operator autocomplete, and it's different //
|
||||
// from the last selected one, then set the state vars that control that autocomplete //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator);
|
||||
if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue))
|
||||
{
|
||||
setOperatorSelectedValue(maybeNewOperatorSelectedValue)
|
||||
setOperatorInputValue(maybeNewOperatorSelectedValue?.label)
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there wasn't a criteria, or we need to reset it (make a new one), then do so //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if (criteria == null || criteriaNeedsReset())
|
||||
{
|
||||
makeNewCriteria();
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// build up the button //
|
||||
/////////////////////////
|
||||
const tooComplex = criteriaParam == "tooComplex";
|
||||
const tooltipEnterDelay = 500;
|
||||
|
||||
let buttonAdditionalStyles: any = {};
|
||||
let buttonContent = <span>{tableForField?.name != tableMetaData.name ? `${tableForField.label}: ` : ""}{fieldMetaData.label}</span>
|
||||
let buttonClassName = "filterNotActive";
|
||||
if (criteriaIsValid)
|
||||
{
|
||||
buttonAdditionalStyles.backgroundColor = accentColor + " !important";
|
||||
buttonAdditionalStyles.borderColor = accentColor + " !important";
|
||||
buttonAdditionalStyles.color = "white !important";
|
||||
buttonClassName = "filterActive";
|
||||
|
||||
let valuesString = FilterUtils.getValuesString(fieldMetaData, criteria, 1, "+N");
|
||||
|
||||
///////////////////////////////////////////
|
||||
// don't show the Equals or In operators //
|
||||
///////////////////////////////////////////
|
||||
let operatorString = (<>{operatorSelectedValue.label} </>);
|
||||
if(operatorSelectedValue.value == QCriteriaOperator.EQUALS || operatorSelectedValue.value == QCriteriaOperator.IN)
|
||||
{
|
||||
operatorString = (<></>)
|
||||
}
|
||||
|
||||
buttonContent = (<><span style={{fontWeight: 700}}>{buttonContent}:</span> <span style={{fontWeight: 400}}>{operatorString}{valuesString}</span></>);
|
||||
}
|
||||
|
||||
const mouseEvents =
|
||||
{
|
||||
onMouseOver: () => handleMouseOverElement(),
|
||||
onMouseOut: () => handleMouseOutElement()
|
||||
};
|
||||
|
||||
let button = fieldMetaData && <Button
|
||||
id={`quickFilter.${fullFieldName}`}
|
||||
className={buttonClassName}
|
||||
{...mouseEvents}
|
||||
sx={{...quickFilterButtonStyles, ...buttonAdditionalStyles, mr: "0.5rem"}}
|
||||
onClick={tooComplex ? noop : handleOpenMenu}
|
||||
disabled={tooComplex}
|
||||
>{buttonContent}</Button>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the criteria on this field is the "tooComplex" sentinel, then wrap the button in a tooltip stating such, and return early. //
|
||||
// note this was part of original design on this widget, but later deprecated... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (tooComplex)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// wrap button in span, so disabled button doesn't cause disabled tooltip //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
return (
|
||||
<Tooltip title={`Your current filter is too complex to do a Quick Filter on ${fieldMetaData.label}. Use the Filter button to edit.`} enterDelay={tooltipEnterDelay} slotProps={{popper: {sx: {top: "-0.75rem!important"}}}}>
|
||||
<span>{button}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** event handler for 'x' button - either resets the criteria or turns off the field.
|
||||
*******************************************************************************/
|
||||
const xClicked = (e: React.MouseEvent<HTMLSpanElement>) =>
|
||||
{
|
||||
e.stopPropagation();
|
||||
if(criteriaIsValid)
|
||||
{
|
||||
resetCriteria(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
handleTurningOffQuickFilterField();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// return the button & menu //
|
||||
//////////////////////////////
|
||||
const widthAndMaxWidth = 250
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// only show the 'x' if it's to clear out a valid criteria on the field, //
|
||||
// or if we were given a callback to remove the quick-filter field from the screen //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
(criteriaIsValid || handleRemoveQuickFilterField) && isMouseOver && <span {...mouseEvents}><XIcon shade={criteriaIsValid ? "accent" : "default"} position="forQuickFilter" onClick={xClicked} /></span>
|
||||
}
|
||||
{
|
||||
isOpen && <Menu open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={closeMenu} sx={{overflow: "visible"}}>
|
||||
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
||||
<Autocomplete
|
||||
id={"criteriaOperator"}
|
||||
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||
options={operatorOptions}
|
||||
value={operatorSelectedValue as any}
|
||||
inputValue={operatorInputValue}
|
||||
onChange={handleOperatorChange}
|
||||
onInputChange={(e, value) => setOperatorInputValue(value)}
|
||||
isOptionEqualToValue={(option, value) => isOperatorOptionEqual(option, value)}
|
||||
getOptionLabel={(option: any) => option.label}
|
||||
autoSelect={true}
|
||||
autoHighlight={true}
|
||||
disableClearable
|
||||
slotProps={{popper: {style: {padding: 0, maxHeight: "unset", width: "250px"}}}}
|
||||
/>
|
||||
</Box>
|
||||
<Box width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="quickFilter filterValuesColumn">
|
||||
<FilterCriteriaRowValues
|
||||
operatorOption={operatorSelectedValue}
|
||||
criteria={criteria}
|
||||
field={fieldMetaData}
|
||||
table={tableForField}
|
||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||
initiallyOpenMultiValuePvs={true} // todo - maybe not?
|
||||
/>
|
||||
</Box>
|
||||
</Menu>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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 Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {useState} from "react";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Component that is the dialog for the user to enter the selection-subset
|
||||
*******************************************************************************/
|
||||
export default function SelectionSubsetDialog(props: { isOpen: boolean; initialValue: number; closeHandler: (value?: number) => void })
|
||||
{
|
||||
const [value, setValue] = useState(props.initialValue);
|
||||
|
||||
const handleChange = (newValue: string) =>
|
||||
{
|
||||
setValue(parseInt(newValue));
|
||||
};
|
||||
|
||||
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
{
|
||||
if (e.key == "Enter" && value)
|
||||
{
|
||||
props.closeHandler(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={props.isOpen} onClose={() => props.closeHandler()} onKeyPress={(e) => keyPressed(e)}>
|
||||
<DialogTitle>Subset of the Query Result</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>How many records do you want to select?</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
name="selection-subset-size"
|
||||
inputProps={{width: "100%", type: "number", min: 1}}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
value={value}
|
||||
sx={{width: "100%"}}
|
||||
onFocus={event => event.target.select()}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<QCancelButton disabled={false} onClickHandler={() => props.closeHandler()} />
|
||||
<QSaveButton label="OK" iconName="check" disabled={value == undefined || isNaN(value)} onClickHandler={() => props.closeHandler(value)} />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
/*******************************************************************************
|
||||
** Component that is the dialog for the user to select a variant on tables with variant backends //
|
||||
*******************************************************************************/
|
||||
export default function TableVariantDialog(props: { isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void })
|
||||
{
|
||||
const [value, setValue] = useState(null);
|
||||
const [dropDownOpen, setDropDownOpen] = useState(false);
|
||||
const [variants, setVariants] = useState(null);
|
||||
|
||||
const handleVariantChange = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||
{
|
||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${props.table.name}`;
|
||||
if (value != null)
|
||||
{
|
||||
localStorage.setItem(tableVariantLocalStorageKey, JSON.stringify(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||
}
|
||||
props.closeHandler(value);
|
||||
};
|
||||
|
||||
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
{
|
||||
if (e.key == "Enter" && value)
|
||||
{
|
||||
props.closeHandler(value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
console.log("queryVariants");
|
||||
try
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
const variants = await qController.tableVariants(props.table.name);
|
||||
console.log(JSON.stringify(variants));
|
||||
setVariants(variants);
|
||||
})();
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.log(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
return variants && (
|
||||
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||
<Autocomplete
|
||||
id="tableVariantId"
|
||||
sx={{width: "400px", marginTop: "10px"}}
|
||||
open={dropDownOpen}
|
||||
size="small"
|
||||
onOpen={() =>
|
||||
{
|
||||
setDropDownOpen(true);
|
||||
}}
|
||||
onClose={() =>
|
||||
{
|
||||
setDropDownOpen(false);
|
||||
}}
|
||||
// @ts-ignore
|
||||
onChange={handleVariantChange}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
options={variants}
|
||||
renderInput={(params) => <TextField {...params} label={props.table.variantTableLabel} />}
|
||||
getOptionLabel={(option) =>
|
||||
{
|
||||
if (typeof option == "object")
|
||||
{
|
||||
return (option as QTableVariant).name;
|
||||
}
|
||||
return option;
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* 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 Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import React, {useContext} from "react";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
interface XIconProps
|
||||
{
|
||||
onClick: (e: React.MouseEvent<HTMLSpanElement>) => void;
|
||||
position: "forQuickFilter" | "forAdvancedQueryPreview" | "default";
|
||||
shade: "default" | "accent" | "accentLight"
|
||||
}
|
||||
|
||||
XIcon.defaultProps = {
|
||||
position: "default",
|
||||
shade: "default"
|
||||
};
|
||||
|
||||
export default function XIcon({onClick, position, shade}: XIconProps): JSX.Element
|
||||
{
|
||||
const {accentColor, accentColorLight} = useContext(QContext)
|
||||
|
||||
//////////////////////////
|
||||
// for default position //
|
||||
//////////////////////////
|
||||
let rest: any = {
|
||||
top: "-0.75rem",
|
||||
left: "-0.5rem",
|
||||
}
|
||||
|
||||
if(position == "forQuickFilter")
|
||||
{
|
||||
rest = {
|
||||
left: "-1.125rem",
|
||||
}
|
||||
}
|
||||
else if(position == "forAdvancedQueryPreview")
|
||||
{
|
||||
rest = {
|
||||
top: "-0.5rem",
|
||||
left: "-0.75rem",
|
||||
}
|
||||
}
|
||||
|
||||
let color;
|
||||
switch (shade)
|
||||
{
|
||||
case "default":
|
||||
color = colors.gray.main;
|
||||
break;
|
||||
case "accent":
|
||||
color = accentColor;
|
||||
break;
|
||||
case "accentLight":
|
||||
color = accentColorLight;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{position: "relative"}}><IconButton sx={{
|
||||
fontSize: "0.75rem",
|
||||
border: `1px solid ${color}`,
|
||||
color: color,
|
||||
padding: "0",
|
||||
background: "#FFFFFF !important",
|
||||
position: "absolute",
|
||||
... rest
|
||||
}} onClick={onClick}><Icon>close</Icon></IconButton></span>
|
||||
)
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
/*
|
||||
* 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 {Box, Skeleton} from "@mui/material";
|
||||
import React from "react";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||
|
||||
|
||||
interface CompositeData
|
||||
{
|
||||
blocks: BlockData[];
|
||||
styleOverrides?: any;
|
||||
layout?: string
|
||||
}
|
||||
|
||||
|
||||
interface CompositeWidgetProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: CompositeData;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Widget which is a list of Blocks.
|
||||
*******************************************************************************/
|
||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
||||
{
|
||||
if (!data || !data.blocks)
|
||||
{
|
||||
return (<Skeleton />);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - these layouts are defined in qqq in the CompositeWidgetData.Layout enum //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
let layout = data?.layout;
|
||||
let boxStyle: any = {};
|
||||
if (layout == "FLEX_ROW_WRAPPED")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.flexDirection = "row";
|
||||
boxStyle.flexWrap = "wrap";
|
||||
boxStyle.gap = "0.5rem";
|
||||
}
|
||||
else if (layout == "FLEX_ROW_SPACE_BETWEEN")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.flexDirection = "row";
|
||||
boxStyle.justifyContent = "space-between"
|
||||
boxStyle.gap = "0.25rem";
|
||||
}
|
||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.flexDirection = "column";
|
||||
boxStyle.fontSize = "0.875rem";
|
||||
boxStyle.fontWeight = 400;
|
||||
boxStyle.borderRight = "1px solid #D0D0D0";
|
||||
}
|
||||
else if (layout == "BADGES_WRAPPER")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.gap = "0.25rem";
|
||||
boxStyle.padding = "0 0.25rem";
|
||||
boxStyle.fontSize = "0.875rem";
|
||||
boxStyle.fontWeight = 400;
|
||||
boxStyle.border = "1px solid gray";
|
||||
boxStyle.borderRadius = "0.5rem";
|
||||
boxStyle.background = "#FFFFFF";
|
||||
}
|
||||
|
||||
if (data?.styleOverrides)
|
||||
{
|
||||
boxStyle = {...boxStyle, ...data.styleOverrides};
|
||||
}
|
||||
|
||||
return (<Box sx={boxStyle} className="compositeWidget">
|
||||
{
|
||||
data.blocks.map((block: BlockData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</Box>);
|
||||
|
||||
}
|
@ -22,20 +22,17 @@ import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Q
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||
import {useLocation} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import TabPanel from "qqq/components/misc/TabPanel";
|
||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||
import HorizontalBarChart from "qqq/components/widgets/charts/barchart/HorizontalBarChart";
|
||||
import DefaultLineChart from "qqq/components/widgets/charts/linechart/DefaultLineChart";
|
||||
import SmallLineChart from "qqq/components/widgets/charts/linechart/SmallLineChart";
|
||||
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 DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
||||
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
||||
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
|
||||
@ -47,8 +44,7 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||
import Widget, {HeaderIcon, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import TableWidget from "./tables/TableWidget";
|
||||
@ -62,10 +58,9 @@ interface Props
|
||||
tableName?: string;
|
||||
entityPrimaryKey?: string;
|
||||
omitWrappingGridContainer: boolean;
|
||||
areChildren?: boolean;
|
||||
childUrlParams?: string;
|
||||
parentWidgetMetaData?: QWidgetMetaData;
|
||||
wrapWidgetsInTabPanels: boolean;
|
||||
areChildren?: boolean
|
||||
childUrlParams?: string
|
||||
parentWidgetMetaData?: QWidgetMetaData
|
||||
}
|
||||
|
||||
DashboardWidgets.defaultProps = {
|
||||
@ -75,12 +70,12 @@ DashboardWidgets.defaultProps = {
|
||||
omitWrappingGridContainer: false,
|
||||
areChildren: false,
|
||||
childUrlParams: "",
|
||||
parentWidgetMetaData: null,
|
||||
wrapWidgetsInTabPanels: false,
|
||||
parentWidgetMetaData: null
|
||||
};
|
||||
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData}: Props): JSX.Element
|
||||
{
|
||||
const location = useLocation();
|
||||
const [widgetData, setWidgetData] = useState([] as any[]);
|
||||
const [widgetCounter, setWidgetCounter] = useState(0);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
@ -89,24 +84,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
let initialSelectedTab = 0;
|
||||
let selectedTabKey: string = null;
|
||||
if(parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||
{
|
||||
selectedTabKey = `qqq.widgets.selectedTabs.${parentWidgetMetaData.name}`
|
||||
if (localStorage.getItem(selectedTabKey))
|
||||
{
|
||||
initialSelectedTab = Number(localStorage.getItem(selectedTabKey));
|
||||
}
|
||||
}
|
||||
const [selectedTab, setSelectedTab] = useState(initialSelectedTab);
|
||||
|
||||
const changeTab = (newValue: number) =>
|
||||
{
|
||||
setSelectedTab(newValue);
|
||||
localStorage.setItem(selectedTabKey, String(newValue));
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setWidgetData([]);
|
||||
@ -125,15 +102,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
||||
setWidgetData(widgetData);
|
||||
setWidgetCounter(widgetCounter + 1);
|
||||
if (widgetData[i])
|
||||
if(widgetData[i])
|
||||
{
|
||||
widgetData[i]["errorLoading"] = false;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
catch(e)
|
||||
{
|
||||
console.error(e);
|
||||
if (widgetData[i])
|
||||
if(widgetData[i])
|
||||
{
|
||||
widgetData[i]["errorLoading"] = true;
|
||||
}
|
||||
@ -146,7 +123,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
const reloadWidget = async (index: number, data: string) =>
|
||||
{
|
||||
(async () =>
|
||||
(async() =>
|
||||
{
|
||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||
setCurrentUrlParams(urlParams);
|
||||
@ -163,7 +140,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData[index]["errorLoading"] = false;
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
catch(e)
|
||||
{
|
||||
console.error(e);
|
||||
if (widgetData[index])
|
||||
@ -174,7 +151,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
forceUpdate();
|
||||
})();
|
||||
};
|
||||
}
|
||||
|
||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||
{
|
||||
@ -201,36 +178,36 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
}
|
||||
}
|
||||
|
||||
if (entityPrimaryKey)
|
||||
if(entityPrimaryKey)
|
||||
{
|
||||
paramMap.set("id", entityPrimaryKey);
|
||||
}
|
||||
|
||||
if (tableName)
|
||||
if(tableName)
|
||||
{
|
||||
paramMap.set("tableName", tableName);
|
||||
}
|
||||
|
||||
if (extraParams)
|
||||
if(extraParams)
|
||||
{
|
||||
let pairs = extraParams.split("&");
|
||||
for (let i = 0; i < pairs.length; i++)
|
||||
{
|
||||
let nameValue = pairs[i].split("=");
|
||||
if (nameValue.length == 2)
|
||||
if(nameValue.length == 2)
|
||||
{
|
||||
paramMap.set(nameValue[0], nameValue[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (childUrlParams)
|
||||
if(childUrlParams)
|
||||
{
|
||||
let pairs = childUrlParams.split("&");
|
||||
for (let i = 0; i < pairs.length; i++)
|
||||
{
|
||||
let nameValue = pairs[i].split("=");
|
||||
if (nameValue.length == 2)
|
||||
if(nameValue.length == 2)
|
||||
{
|
||||
paramMap.set(nameValue[0], nameValue[1]);
|
||||
}
|
||||
@ -250,26 +227,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
|
||||
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
||||
{
|
||||
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||
if (widgetMetaData && widgetMetaData.icons)
|
||||
{
|
||||
const topRightInsideCardIcon = widgetMetaData.icons.get("topRightInsideCard");
|
||||
if (topRightInsideCardIcon)
|
||||
{
|
||||
labelAdditionalComponentsRight.push(new HeaderIcon(topRightInsideCardIcon.name, topRightInsideCardIcon.path, topRightInsideCardIcon.color, "topRightInsideCard"));
|
||||
}
|
||||
}
|
||||
|
||||
const labelAdditionalComponentsLeft: LabelComponent[] = [];
|
||||
if (widgetMetaData && widgetMetaData.icons)
|
||||
{
|
||||
const topLeftInsideCardIcon = widgetMetaData.icons.get("topLeftInsideCard");
|
||||
if (topLeftInsideCardIcon)
|
||||
{
|
||||
labelAdditionalComponentsLeft.push(new HeaderIcon(topLeftInsideCardIcon.name, topLeftInsideCardIcon.path, topLeftInsideCardIcon.color, "topLeftInsideCard"));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
|
||||
{
|
||||
@ -281,7 +238,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetIndex={i}
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
reloadWidgetCallback={reloadWidget}
|
||||
storeDropdownSelections={widgetMetaData.storeDropdownSelections}
|
||||
/>
|
||||
)
|
||||
@ -313,10 +270,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<StackedBarChart data={widgetData[i]?.chartData} chartSubheaderData={widgetData[i]?.chartSubheaderData} />
|
||||
<StackedBarChart data={widgetData[i]?.chartData}/>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
@ -327,8 +282,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
showReloadControl={false}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
||||
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
||||
@ -342,8 +295,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
||||
<Box padding="1rem" sx={{width: "100%"}}>
|
||||
@ -359,10 +310,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetMetaData={widgetMetaData}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
widgetData={widgetData[i]}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<Box>
|
||||
<Box px={3} pt={0} pb={2}>
|
||||
<MDTypography component="div" variant="button" color="text" fontWeight="light">
|
||||
{
|
||||
widgetData && widgetData[i] && widgetData[i].html ? (
|
||||
@ -392,11 +341,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
isChild={areChildren}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<StatisticsCard
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
increaseIsGood={true}
|
||||
/>
|
||||
@ -435,13 +381,10 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<div>
|
||||
<PieChart
|
||||
chartData={widgetData[i]?.chartData}
|
||||
chartSubheaderData={widgetData[i]?.chartSubheaderData}
|
||||
description={widgetData[i]?.description}
|
||||
/>
|
||||
</div>
|
||||
@ -472,8 +415,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<DefaultLineChart sx={{alignItems: "center"}}
|
||||
data={widgetData[i]?.chartData}
|
||||
@ -495,39 +436,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
{
|
||||
widgetMetaData.type === "fieldValueList" && (
|
||||
widgetData && widgetData[i] &&
|
||||
<FieldValueListWidget
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "composite" && (
|
||||
<Widget
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "block" && (
|
||||
<Widget
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
isChild={areChildren}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={widgetData[i]} />
|
||||
</Widget>
|
||||
<FieldValueListWidget
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
@ -548,75 +461,32 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
if(wrapWidgetsInTabPanels)
|
||||
{
|
||||
omitWrappingGridContainer = true;
|
||||
}
|
||||
|
||||
const body: JSX.Element =
|
||||
(
|
||||
<>
|
||||
{
|
||||
widgetMetaDataList.map((widgetMetaData, i) =>
|
||||
{
|
||||
let renderedWidget = widgetMetaData ? renderWidget(widgetMetaData, i) : (<></>);
|
||||
|
||||
if (!omitWrappingGridContainer)
|
||||
{
|
||||
// @ts-ignore
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderedWidget}
|
||||
</Grid>);
|
||||
}
|
||||
|
||||
if (wrapWidgetsInTabPanels)
|
||||
{
|
||||
renderedWidget = (<TabPanel index={i} value={selectedTab} style={{
|
||||
padding: 0,
|
||||
margin: "-1rem",
|
||||
width: "calc(100% + 2rem)"
|
||||
}}>
|
||||
{renderedWidget}
|
||||
</TabPanel>);
|
||||
}
|
||||
|
||||
return (<React.Fragment key={`${widgetMetaData.name}-${i}`}>{renderedWidget}</React.Fragment>)
|
||||
})
|
||||
widgetMetaDataList.map((widgetMetaData, i) => (
|
||||
omitWrappingGridContainer
|
||||
? widgetMetaData && renderWidget(widgetMetaData, i)
|
||||
:
|
||||
widgetMetaData && <Grid id={widgetMetaData.name} key={`${widgetMetaData.name}-${i}`} item lg={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderWidget(widgetMetaData, i)}
|
||||
</Grid>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
|
||||
const tabs = widgetMetaDataList && wrapWidgetsInTabPanels ?
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1.5, ml: -2, mr: -2, mt: -3,
|
||||
"& .MuiTabs-scroller": {
|
||||
ml: 0
|
||||
}
|
||||
}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
{widgetMetaDataList.map((widgetMetaData, i) => (
|
||||
<Tab key={widgetMetaData.name} label={widgetMetaData.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
: <></>
|
||||
|
||||
return (
|
||||
widgetCount > 0 ? (
|
||||
<>
|
||||
{tabs}
|
||||
{
|
||||
omitWrappingGridContainer ? body : (
|
||||
<Grid container spacing={2.5}>
|
||||
{body}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</>
|
||||
omitWrappingGridContainer ? body :
|
||||
(
|
||||
<Grid container spacing={3} pb={4}>
|
||||
{body}
|
||||
</Grid>
|
||||
)
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import Client from "qqq/utils/qqq/Client";
|
||||
//////////////////////////////////////////////
|
||||
export interface ParentWidgetData
|
||||
{
|
||||
label?: string;
|
||||
dropdownLabelList: string[];
|
||||
dropdownNameList: string[];
|
||||
dropdownDataList: {
|
||||
@ -43,9 +42,7 @@ export interface ParentWidgetData
|
||||
childWidgetNameList: string[];
|
||||
dropdownNeedsSelectedText?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
csvData?: any[][];
|
||||
icon?: string;
|
||||
layoutType: string;
|
||||
}
|
||||
|
||||
|
||||
@ -58,7 +55,7 @@ interface Props
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetIndex: number;
|
||||
data: ParentWidgetData;
|
||||
reloadWidgetCallback?: (params: string) => void;
|
||||
reloadWidgetCallback?: (widgetIndex: number, params: string) => void;
|
||||
entityPrimaryKey?: string;
|
||||
tableName?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
@ -66,8 +63,7 @@ interface Props
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props,): JSX.Element
|
||||
function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidgetCallback, entityPrimaryKey, tableName, storeDropdownSelections}: Props, ): JSX.Element
|
||||
{
|
||||
const [childUrlParams, setChildUrlParams] = useState((urlParams) ? urlParams : "");
|
||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||
@ -84,40 +80,29 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (qInstance && data && data.childWidgetNameList)
|
||||
if(qInstance && data && data.childWidgetNameList)
|
||||
{
|
||||
let widgetMetaDataList = [] as QWidgetMetaData[];
|
||||
data?.childWidgetNameList.forEach((widgetName: string) =>
|
||||
{
|
||||
widgetMetaDataList.push(qInstance.widgets.get(widgetName));
|
||||
});
|
||||
})
|
||||
setWidgets(widgetMetaDataList);
|
||||
}
|
||||
}, [qInstance, data, childUrlParams]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
setChildUrlParams(urlParams);
|
||||
}, [urlParams]);
|
||||
|
||||
const parentReloadWidgetCallback = (data: string) =>
|
||||
{
|
||||
setChildUrlParams(data);
|
||||
reloadWidgetCallback(data);
|
||||
};
|
||||
reloadWidgetCallback(widgetIndex, data);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this parent widget is in card form, and its children are too, then we need some px //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
const parentIsCard = widgetMetaData && widgetMetaData.isCard;
|
||||
const childrenAreCards = widgetMetaData && widgets && widgets[0] && widgets[0].isCard;
|
||||
const px = (parentIsCard && childrenAreCards) ? 3 : 0;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a parent, which is not a card, then we need to omit the padding, i think, on the Widget that ultimately gets rendered //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const omitPadding = !parentIsCard;
|
||||
const px = (widgetMetaData && widgetMetaData.isCard && widgets && widgets[0] && widgets[0].isCard) ? 3 : 0;
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
qInstance && data ? (
|
||||
<Widget
|
||||
@ -125,10 +110,9 @@ function ParentWidget({urlParams, widgetMetaData, widgetIndex, data, reloadWidge
|
||||
widgetData={data}
|
||||
storeDropdownSelections={storeDropdownSelections}
|
||||
reloadWidgetCallback={parentReloadWidgetCallback}
|
||||
omitPadding={omitPadding}
|
||||
>
|
||||
<Box sx={{height: "100%", width: "100%"}} px={px}>
|
||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData} wrapWidgetsInTabPanels={data.layoutType?.toLowerCase() == "tabs"} />
|
||||
<DashboardWidgets widgetMetaDataList={widgets} entityPrimaryKey={entityPrimaryKey} tableName={tableName} childUrlParams={childUrlParams} areChildren={true} parentWidgetMetaData={widgetMetaData}/>
|
||||
</Box>
|
||||
</Widget>
|
||||
) : null
|
||||
|
@ -25,15 +25,14 @@ import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import parse from "html-react-parser";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import WidgetDropdownMenu, {DropdownOption} from "qqq/components/widgets/components/WidgetDropdownMenu";
|
||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import {Link, NavigateFunction, useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/components/legacy/colors";
|
||||
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
|
||||
|
||||
export interface WidgetData
|
||||
{
|
||||
@ -44,11 +43,9 @@ export interface WidgetData
|
||||
id: string,
|
||||
label: string
|
||||
}[][];
|
||||
dropdownDefaultValueList?: string[];
|
||||
dropdownNeedsSelectedText?: string;
|
||||
hasPermission?: boolean;
|
||||
errorLoading?: boolean;
|
||||
|
||||
[other: string]: any;
|
||||
}
|
||||
|
||||
@ -56,9 +53,7 @@ export interface WidgetData
|
||||
interface Props
|
||||
{
|
||||
labelAdditionalComponentsLeft: LabelComponent[];
|
||||
labelAdditionalElementsLeft: JSX.Element[];
|
||||
labelAdditionalComponentsRight: LabelComponent[];
|
||||
labelBoxAdditionalSx?: any;
|
||||
widgetMetaData?: QWidgetMetaData;
|
||||
widgetData?: WidgetData;
|
||||
children: JSX.Element;
|
||||
@ -67,7 +62,6 @@ interface Props
|
||||
isChild?: boolean;
|
||||
footerHTML?: string;
|
||||
storeDropdownSelections?: boolean;
|
||||
omitPadding: boolean;
|
||||
}
|
||||
|
||||
Widget.defaultProps = {
|
||||
@ -76,10 +70,7 @@ Widget.defaultProps = {
|
||||
widgetMetaData: {},
|
||||
widgetData: {},
|
||||
labelAdditionalComponentsLeft: [],
|
||||
labelAdditionalElementsLeft: [],
|
||||
labelAdditionalComponentsRight: [],
|
||||
labelBoxAdditionalSx: {},
|
||||
omitPadding: false,
|
||||
};
|
||||
|
||||
|
||||
@ -97,64 +88,34 @@ export class LabelComponent
|
||||
{
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (<div>Unsupported component type</div>);
|
||||
};
|
||||
return (<div>Unsupported component type</div>)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export class HeaderIcon extends LabelComponent
|
||||
export class HeaderLink extends LabelComponent
|
||||
{
|
||||
iconName: string;
|
||||
iconPath: string;
|
||||
color: string;
|
||||
coloredBG: boolean;
|
||||
role: string;
|
||||
label: string;
|
||||
to: string
|
||||
|
||||
iconColor: string;
|
||||
bgColor: string;
|
||||
|
||||
constructor(iconName: string, iconPath: string, color: string, role?: string, coloredBG: boolean = true)
|
||||
constructor(label: string, to: string)
|
||||
{
|
||||
super();
|
||||
this.iconName = iconName;
|
||||
this.iconPath = iconPath;
|
||||
this.color = color;
|
||||
this.role = role;
|
||||
this.coloredBG = coloredBG;
|
||||
|
||||
this.iconColor = this.coloredBG ? "#FFFFFF" : this.color;
|
||||
this.bgColor = this.coloredBG ? this.color : "none";
|
||||
this.label = label;
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
const styles: any = {
|
||||
width: "1.75rem",
|
||||
height: "1.75rem",
|
||||
color: this.iconColor,
|
||||
backgroundColor: this.bgColor,
|
||||
borderRadius: "0.25rem"
|
||||
};
|
||||
|
||||
if(this.role == "topLeftInsideCard")
|
||||
{
|
||||
styles["order"] = -1;
|
||||
styles["marginRight"] = "0.5rem";
|
||||
}
|
||||
|
||||
if (this.iconPath)
|
||||
{
|
||||
return (<Box sx={{textAlign: "center", ...styles}}><img src={this.iconPath} width="16" height="16" /></Box>);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<Icon sx={{padding: "0.25rem", ...styles}} fontSize="small">{this.iconName}</Icon>);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
|
||||
{this.to ? <Link to={this.to}>{this.label}</Link> : null}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -180,17 +141,45 @@ export class AddNewRecordButton extends LabelComponent
|
||||
|
||||
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
|
||||
{
|
||||
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`);
|
||||
};
|
||||
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
|
||||
}
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (
|
||||
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="-0.5rem">
|
||||
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
|
||||
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
export class ExportDataButton extends LabelComponent
|
||||
{
|
||||
callbackToExport: any;
|
||||
tooltipTitle: string;
|
||||
isDisabled: boolean;
|
||||
|
||||
constructor(callbackToExport: any, isDisabled = false, tooltipTitle: string = "Export")
|
||||
{
|
||||
super();
|
||||
this.callbackToExport = callbackToExport;
|
||||
this.isDisabled = isDisabled;
|
||||
this.tooltipTitle = tooltipTitle;
|
||||
}
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (
|
||||
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
||||
<Tooltip title={this.tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callbackToExport()} disabled={this.isDisabled}><Icon>save_alt</Icon></Button></Tooltip>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -200,115 +189,45 @@ export class AddNewRecordButton extends LabelComponent
|
||||
export class Dropdown extends LabelComponent
|
||||
{
|
||||
label: string;
|
||||
dropdownMetaData: any;
|
||||
options: DropdownOption[];
|
||||
dropdownDefaultValue?: string;
|
||||
dropdownName: string;
|
||||
onChangeCallback: any;
|
||||
|
||||
constructor(label: string, dropdownMetaData: any, options: DropdownOption[], dropdownDefaultValue: string, dropdownName: string, onChangeCallback: any)
|
||||
constructor(label: string, options: DropdownOption[], dropdownName: string, onChangeCallback: any)
|
||||
{
|
||||
super();
|
||||
this.label = label;
|
||||
this.dropdownMetaData = dropdownMetaData;
|
||||
this.options = options;
|
||||
this.dropdownDefaultValue = dropdownDefaultValue;
|
||||
this.dropdownName = dropdownName;
|
||||
this.onChangeCallback = onChangeCallback;
|
||||
}
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
const label = `Select ${this.label}`;
|
||||
let defaultValue = null;
|
||||
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${this.dropdownName}`;
|
||||
if (args.widgetProps.storeDropdownSelections)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||
// originally we used the full object from localStorage - but - in case the label //
|
||||
// changed since it was stored, we'll instead just find the option by id (or in case that //
|
||||
// option isn't available anymore, then we'll select nothing instead of a missing value //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
const localStorageOption = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
if(localStorageOption)
|
||||
{
|
||||
const id = localStorageOption.id;
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
if (this.options[i].id == id)
|
||||
{
|
||||
defaultValue = this.options[i]
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log(`Error getting default value for dropdown [${this.dropdownName}] from local storage`, e)
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there wasn't a value selected, but there is a default from the backend, then use it. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (defaultValue == null && this.dropdownDefaultValue != null)
|
||||
{
|
||||
for (let i = 0; i < this.options.length; i++)
|
||||
{
|
||||
if(this.options[i].id == this.dropdownDefaultValue)
|
||||
{
|
||||
defaultValue = this.options[i];
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
|
||||
if (args.widgetProps.storeDropdownSelections)
|
||||
{
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(defaultValue));
|
||||
}
|
||||
|
||||
this.onChangeCallback(label, defaultValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a 'label for null value' (and no default from the backend), //
|
||||
// then add that as an option (and select it if nothing else was selected) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
let options = this.options;
|
||||
if (this.dropdownMetaData.labelForNullValue && !this.dropdownDefaultValue)
|
||||
{
|
||||
const nullOption = {id: null as string, label: this.dropdownMetaData.labelForNullValue};
|
||||
options = [nullOption, ...this.options];
|
||||
|
||||
if (!defaultValue)
|
||||
{
|
||||
defaultValue = nullOption;
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// see if an existing value is stored in local storage, and if so set it in dropdown //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
|
||||
args.dropdownData[args.componentIndex] = defaultValue?.id;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb={2} sx={{float: "right"}}>
|
||||
<WidgetDropdownMenu
|
||||
<Box my={2} sx={{float: "right"}}>
|
||||
<DropdownMenu
|
||||
name={this.dropdownName}
|
||||
defaultValue={defaultValue}
|
||||
sx={{marginLeft: "1rem"}}
|
||||
label={label}
|
||||
startIcon={this.dropdownMetaData.startIconName}
|
||||
allowBackAndForth={this.dropdownMetaData.allowBackAndForth}
|
||||
backAndForthInverted={this.dropdownMetaData.backAndForthInverted}
|
||||
disableClearable={this.dropdownMetaData.disableClearable}
|
||||
dropdownOptions={options}
|
||||
sx={{width: 200, marginLeft: "15px"}}
|
||||
label={`Select ${this.label}`}
|
||||
dropdownOptions={this.options}
|
||||
onChangeCallback={this.onChangeCallback}
|
||||
width={this.dropdownMetaData.width ?? 225}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -327,14 +246,12 @@ export class ReloadControl extends LabelComponent
|
||||
|
||||
render = (args: LabelComponentRenderArgs): JSX.Element =>
|
||||
{
|
||||
return (<Typography key={1} variant="body2" py={0} px={0} display="inline" position="relative" top="-0.25rem">
|
||||
<Tooltip title="Refresh">
|
||||
<Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}>
|
||||
<Icon sx={{color: colors.gray.main, fontSize: 1.125}}>refresh</Icon>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Typography>);
|
||||
};
|
||||
return (
|
||||
<Typography variant="body2" py={2} px={0} display="inline" position="relative" top="-0.375rem">
|
||||
<Tooltip title="Refresh"><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={() => this.callback()}><Icon>refresh</Icon></Button></Tooltip>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -348,29 +265,15 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [dropdownData, setDropdownData] = useState([]);
|
||||
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
|
||||
const [reloading, setReloading] = useState(false);
|
||||
const [dropdownDataJSON, setDropdownDataJSON] = useState("");
|
||||
const [labelComponentsLeft, setLabelComponentsLeft] = useState([] as LabelComponent[]);
|
||||
const [labelComponentsRight, setLabelComponentsRight] = useState([] as LabelComponent[]);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// support for using widget (data) label as page header, w/o it disappearing if dropdowns are changed //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const [lastSeenLabel, setLastSeenLabel] = useState("");
|
||||
const [usingLabelAsTitle, setUsingLabelAsTitle] = useState(false);
|
||||
|
||||
function renderComponent(component: LabelComponent, componentIndex: number)
|
||||
{
|
||||
if(component && component.render)
|
||||
{
|
||||
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Request to render a null component or component without a render function...");
|
||||
console.log(JSON.stringify(component));
|
||||
return (<></>);
|
||||
}
|
||||
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload});
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
@ -430,15 +333,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
props.widgetData.dropdownDataList?.map((dropdownData: any, index: number) =>
|
||||
{
|
||||
// console.log(`${props.widgetMetaData.name} building a Dropdown, data is: ${dropdownData}`);
|
||||
let defaultValue = null;
|
||||
if(props.widgetData.dropdownDefaultValueList && props.widgetData.dropdownDefaultValueList.length >= index)
|
||||
{
|
||||
defaultValue = props.widgetData.dropdownDefaultValueList[index];
|
||||
}
|
||||
if(props.widgetData?.dropdownLabelList && props.widgetData?.dropdownLabelList[index] && props.widgetMetaData?.dropdowns && props.widgetMetaData?.dropdowns[index] && props.widgetData?.dropdownNameList && props.widgetData?.dropdownNameList[index])
|
||||
{
|
||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], props.widgetMetaData.dropdowns[index], dropdownData, defaultValue, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||
}
|
||||
updatedStateLabelComponentsRight.push(new Dropdown(props.widgetData.dropdownLabelList[index], dropdownData, props.widgetData.dropdownNameList[index], handleDataChange));
|
||||
});
|
||||
setLabelComponentsRight(updatedStateLabelComponentsRight);
|
||||
}
|
||||
@ -477,7 +372,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
throw (`Could not find table name for label ${tableName}`);
|
||||
throw(`Could not find table name for label ${tableName}`);
|
||||
}
|
||||
|
||||
dropdownData[index] = (changedData) ? changedData.id : null;
|
||||
@ -499,7 +394,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
reloadWidget(dropdownData);
|
||||
reloadWidget(dropdownData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -527,35 +422,18 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
{
|
||||
console.log(`No reload widget callback in ${props.widgetMetaData.label}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onExportClick = () =>
|
||||
const toggleFullScreenWidget = () =>
|
||||
{
|
||||
if (props.widgetData?.csvData)
|
||||
if (fullScreenWidgetClassName)
|
||||
{
|
||||
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
||||
HtmlUtils.download(fileName, csv);
|
||||
setFullScreenWidgetClassName("");
|
||||
}
|
||||
else
|
||||
{
|
||||
alert("There is no data available to export.");
|
||||
setFullScreenWidgetClassName("fullScreenWidget");
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// add the export button to the label's left elements, if the meta-data says to show it //
|
||||
// don't do this for 2 types which themselves add the button (and have custom code to do the export) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let localLabelAdditionalElementsLeft = [...props.labelAdditionalElementsLeft];
|
||||
if (props.widgetMetaData?.showExportButton && props.widgetMetaData?.type !== "table" && props.widgetMetaData?.type !== "childRecordList")
|
||||
{
|
||||
if(!localLabelAdditionalElementsLeft)
|
||||
{
|
||||
localLabelAdditionalElementsLeft = [];
|
||||
}
|
||||
|
||||
localLabelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
||||
}
|
||||
|
||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||
@ -563,7 +441,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
const isSet = (v: any): boolean =>
|
||||
{
|
||||
return (v !== null && v !== undefined);
|
||||
};
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components //
|
||||
@ -572,70 +450,36 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
if (hasPermission)
|
||||
{
|
||||
needLabelBox ||= (labelComponentsLeft && labelComponentsLeft.length > 0);
|
||||
needLabelBox ||= (localLabelAdditionalElementsLeft && localLabelAdditionalElementsLeft.length > 0);
|
||||
needLabelBox ||= (labelComponentsRight && labelComponentsRight.length > 0);
|
||||
needLabelBox ||= isSet(props.widgetData?.icon);
|
||||
needLabelBox ||= isSet(props.widgetMetaData?.icon);
|
||||
needLabelBox ||= isSet(props.widgetData?.label);
|
||||
needLabelBox ||= isSet(props.widgetMetaData?.label);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// first look for a label in the widget data, which would override that in the metadata //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
const isParentWidget = props.widgetMetaData.type == "parentWidget"; // todo - do we need to know top-level parent, vs. a nested parent?
|
||||
let labelToUse = props.widgetData?.label ?? props.widgetMetaData?.label;
|
||||
|
||||
if(!labelToUse)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// prevent the label from disappearing, especially when it's being used as the page header //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(lastSeenLabel && isParentWidget && usingLabelAsTitle)
|
||||
{
|
||||
labelToUse = lastSeenLabel;
|
||||
}
|
||||
}
|
||||
|
||||
let labelElement = (
|
||||
<Typography sx={{cursor: "default", pl: "auto", fontWeight: 600}} variant={isParentWidget && (props.widgetData.isLabelPageTitle || usingLabelAsTitle) ? "h3" : "h6"} display="inline">
|
||||
{labelToUse}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if(labelToUse && labelToUse != lastSeenLabel)
|
||||
{
|
||||
setLastSeenLabel(labelToUse)
|
||||
setUsingLabelAsTitle(props.widgetData.isLabelPageTitle);
|
||||
}
|
||||
|
||||
if (props.widgetMetaData.tooltip)
|
||||
{
|
||||
labelElement = <Tooltip title={props.widgetMetaData.tooltip} arrow={false} followCursor={true} placement="bottom-start">{labelElement}</Tooltip>;
|
||||
}
|
||||
|
||||
const isTable = props.widgetMetaData.type == "table";
|
||||
|
||||
const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true;
|
||||
const widgetContent =
|
||||
<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" alignItems="baseline">
|
||||
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
|
||||
<Box pt={2} pb={1}>
|
||||
{
|
||||
hasPermission ?
|
||||
props.widgetMetaData?.icon && (
|
||||
<Box ml={1} mr={2} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
<Box
|
||||
ml={3}
|
||||
mt={-4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{props.widgetMetaData.icon}
|
||||
@ -643,34 +487,49 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
</Box>
|
||||
) :
|
||||
(
|
||||
<Box ml={3} mt={-4} sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
<Box
|
||||
ml={3}
|
||||
mt={-4}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "64px",
|
||||
height: "64px",
|
||||
borderRadius: "8px",
|
||||
background: colors.info.main,
|
||||
color: "#ffffff",
|
||||
float: "left"
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">lock</Icon>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && labelToUse && (labelElement)
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// first look for a label in the widget data, which would override that in the metadata //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
hasPermission && props.widgetData?.label ? (
|
||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={2} display="inline-block">
|
||||
{props.widgetData.label}
|
||||
</Typography>
|
||||
) : (
|
||||
hasPermission && props.widgetMetaData?.label && (
|
||||
<Typography sx={{position: "relative", top: -4}} variant="h6" fontWeight="medium" pl={3} display="inline-block">
|
||||
{props.widgetMetaData.label}
|
||||
</Typography>
|
||||
)
|
||||
)
|
||||
}
|
||||
{
|
||||
hasPermission && (
|
||||
labelComponentsLeft.map((component, i) =>
|
||||
{
|
||||
return (<React.Fragment key={i}>{renderComponent(component, i)}</React.Fragment>);
|
||||
return (<span key={i}>{renderComponent(component, i)}</span>);
|
||||
})
|
||||
)
|
||||
}
|
||||
{localLabelAdditionalElementsLeft}
|
||||
</Box>
|
||||
<Box>
|
||||
{
|
||||
@ -685,12 +544,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
</Box>
|
||||
}
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// turning this off... for now. maybe make a property in future //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0", mx:-2}} /> : <Box height="0.375rem" />)
|
||||
*/
|
||||
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem" />)
|
||||
}
|
||||
{
|
||||
errorLoading ? (
|
||||
@ -700,7 +554,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
</Box>
|
||||
) : (
|
||||
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
|
||||
<Box pb={3} sx={{width: "100%", textAlign: "right"}}>
|
||||
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}>
|
||||
<Typography variant="body2">
|
||||
{props.widgetData?.dropdownNeedsSelectedText}
|
||||
</Typography>
|
||||
@ -716,27 +570,16 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
||||
}
|
||||
{
|
||||
!errorLoading && props?.footerHTML && (
|
||||
<Box mt={isTable ? "36px" : 1} ml={isTable ? 0 : 3} mr={isTable ? 0 : 3} mb={isTable ? "-12px" : 2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
||||
<Box mt={1} ml={3} mr={3} mb={2} sx={{fontWeight: 300, color: "#7b809a", display: "flex", alignContent: "flex-end", fontSize: "14px"}}>{parse(props.footerHTML)}</Box>
|
||||
)
|
||||
}
|
||||
</Box>;
|
||||
|
||||
const padding = props.omitPadding ? "auto" : "24px 16px";
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// try to make tables fill their entire "parent" //
|
||||
///////////////////////////////////////////////////
|
||||
let noCardMarginBottom = "unset";
|
||||
if(isTable)
|
||||
{
|
||||
noCardMarginBottom = "-8px";
|
||||
}
|
||||
|
||||
return props.widgetMetaData?.isCard
|
||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%", p: padding}} className="widget inCard">
|
||||
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
|
||||
{widgetContent}
|
||||
</Card>
|
||||
: <span style={{width: "100%", padding: padding, marginBottom: noCardMarginBottom}} className="widget noCard">{widgetContent}</span>;
|
||||
: widgetContent;
|
||||
}
|
||||
|
||||
export default Widget;
|
||||
|
@ -1,90 +0,0 @@
|
||||
/*
|
||||
* 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 {Alert, Skeleton} from "@mui/material";
|
||||
import React from "react";
|
||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import DividerBlock from "qqq/components/widgets/blocks/DividerBlock";
|
||||
import NumberIconBadgeBlock from "qqq/components/widgets/blocks/NumberIconBadgeBlock";
|
||||
import ProgressBarBlock from "qqq/components/widgets/blocks/ProgressBarBlock";
|
||||
import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRowDetailRowBlock";
|
||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
|
||||
|
||||
interface WidgetBlockProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
block: BlockData;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Component to render a single Block in the widget framework!
|
||||
*******************************************************************************/
|
||||
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
|
||||
{
|
||||
if(!block)
|
||||
{
|
||||
return (<Skeleton />);
|
||||
}
|
||||
|
||||
if(!block.values)
|
||||
{
|
||||
block.values = {};
|
||||
}
|
||||
|
||||
if(!block.styles)
|
||||
{
|
||||
block.styles = {};
|
||||
}
|
||||
|
||||
if(block.blockTypeName == "COMPOSITE")
|
||||
{
|
||||
// @ts-ignore - special case for composite type block...
|
||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
||||
}
|
||||
|
||||
switch(block.blockTypeName)
|
||||
{
|
||||
case "TEXT":
|
||||
return (<TextBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "NUMBER_ICON_BADGE":
|
||||
return (<NumberIconBadgeBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "UP_OR_DOWN_NUMBER":
|
||||
return (<UpOrDownNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "TABLE_SUB_ROW_DETAIL_ROW":
|
||||
return (<TableSubRowDetailRowBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "PROGRESS_BAR":
|
||||
return (<ProgressBarBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "DIVIDER":
|
||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "BIG_NUMBER":
|
||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
default:
|
||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
||||
}
|
||||
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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 Button from "@mui/material/Button";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {WidgetData} from "qqq/components/widgets/Widget";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class used by Widgets
|
||||
**
|
||||
*******************************************************************************/
|
||||
export class WidgetUtils
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static generateExportButton = (onExportClick: () => void): JSX.Element =>
|
||||
{
|
||||
return (<Typography key={1} variant="body2" py={0} px={0} display="inline" position="relative" top="-0.25rem">
|
||||
<Tooltip title="Export">
|
||||
<Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={false}>
|
||||
<Icon sx={{color: colors.gray.main, fontSize: 1.125}}>save_alt</Icon>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Typography>);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static widgetCsvDataToString = (data: WidgetData): string =>
|
||||
{
|
||||
function isNumeric(x: any)
|
||||
{
|
||||
return !isNaN(Number(x));
|
||||
}
|
||||
|
||||
let csv = "";
|
||||
for (let i = 0; i < data.csvData.length; i++)
|
||||
{
|
||||
for (let j = 0; j < data.csvData[i].length; j++)
|
||||
{
|
||||
if (j > 0)
|
||||
{
|
||||
csv += ",";
|
||||
}
|
||||
|
||||
let cell = data.csvData[i][j];
|
||||
if (cell && isNumeric(String(cell)))
|
||||
{
|
||||
csv += cell;
|
||||
}
|
||||
else
|
||||
{
|
||||
csv += `"${ValueUtils.cleanForCsv(cell)}"`;
|
||||
}
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
return (csv);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static makeExportFileName = (data: WidgetData, widgetMetaData: QWidgetMetaData): string =>
|
||||
{
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
return (fileName);
|
||||
};
|
||||
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... a big number, optionally with some other stuff.
|
||||
**
|
||||
** ${heading}
|
||||
** ${number} ${context}
|
||||
*******************************************************************************/
|
||||
export default function BigNumberBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
let flexJustifyContent = "normal";
|
||||
let flexAlignItems = "baseline";
|
||||
|
||||
return (
|
||||
<div style={{width: data.styles.width ?? "auto"}}>
|
||||
|
||||
<div style={{fontWeight: "700", fontSize: "0.875rem", color: "#3D3D3D"}}>
|
||||
<BlockElementWrapper data={data} slot="heading">
|
||||
<span>{data.values.heading}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
|
||||
<div style={{display: "flex", alignItems: flexAlignItems, justifyContent: flexJustifyContent}}>
|
||||
|
||||
<div style={{display: "flex", alignItems: "baseline"}}>
|
||||
<div style={{fontWeight: "700", fontSize: "2rem", marginRight: "0.25rem"}}>
|
||||
<BlockElementWrapper data={data} slot="number">
|
||||
<span style={{color: data.styles.numberColor}}>{data.values.number}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
{
|
||||
data.values.context &&
|
||||
<div style={{fontWeight: "500", fontSize: "0.875rem", color: "#7b809a"}}>
|
||||
<BlockElementWrapper data={data} slot="context">
|
||||
<span>{data.values.context}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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 {Tooltip} from "@mui/material";
|
||||
import React, {ReactElement} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
interface BlockElementWrapperProps
|
||||
{
|
||||
data: BlockData;
|
||||
slot: string
|
||||
linkProps?: any;
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** For Blocks - wrap their "slot" elements with an optional tooltip and/or link
|
||||
*******************************************************************************/
|
||||
export default function BlockElementWrapper({data, slot, linkProps, children}: BlockElementWrapperProps): JSX.Element
|
||||
{
|
||||
let link: BlockLink;
|
||||
let tooltip: BlockTooltip;
|
||||
|
||||
if(slot)
|
||||
{
|
||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
||||
if(!link)
|
||||
{
|
||||
link = data.link;
|
||||
}
|
||||
|
||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
||||
if(!tooltip)
|
||||
{
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
link = data.link;
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
|
||||
let rs = children;
|
||||
|
||||
if(link)
|
||||
{
|
||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>
|
||||
}
|
||||
|
||||
if(tooltip)
|
||||
{
|
||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
|
||||
|
||||
// @ts-ignore - placement possible values
|
||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
|
||||
export interface BlockData
|
||||
{
|
||||
blockTypeName: string;
|
||||
|
||||
tooltip?: BlockTooltip;
|
||||
link?: BlockLink;
|
||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
||||
linkMap?: {[slot: string]: BlockLink};
|
||||
|
||||
values: any;
|
||||
styles?: any;
|
||||
}
|
||||
|
||||
|
||||
export interface BlockTooltip
|
||||
{
|
||||
title: string;
|
||||
placement: string;
|
||||
}
|
||||
|
||||
|
||||
export interface BlockLink
|
||||
{
|
||||
href: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
|
||||
export interface StandardBlockComponentProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: BlockData;
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* 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 {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders a simple dividing line
|
||||
** margins & width are set such that it covers the padding of a card.
|
||||
** if we need to use it differently, a style attribute should be added to its backend data.
|
||||
*******************************************************************************/
|
||||
export default function DividerBlock({}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (<div style={{margin: "1rem -1rem", width: "calc(100% + 2rem)", borderBottom: "1px solid #E0E0E0"}} />);
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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 Icon from "@mui/material/Icon";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... a number, and an icon, like a badge.
|
||||
**
|
||||
** ${number} ${icon}
|
||||
*******************************************************************************/
|
||||
export default function NumberIconBadgeBlock({data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<div style={{display: "inline-block", whiteSpace: "nowrap", color: data.styles.color}}>
|
||||
{
|
||||
data.values.number &&
|
||||
<BlockElementWrapper data={data} slot="number">
|
||||
<span style={{color: data.styles.color, fontSize: "0.875rem"}}>{data.values.number}</span>
|
||||
</BlockElementWrapper>
|
||||
}
|
||||
{
|
||||
data.values.iconName &&
|
||||
<BlockElementWrapper data={data} slot="icon">
|
||||
<Icon style={{color: data.styles.color, fontSize: "1rem", position: "relative", top: "3px"}}>{data.values.iconName}</Icon>
|
||||
</BlockElementWrapper>
|
||||
}
|
||||
</div>);
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* 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 Typography from "@mui/material/Typography";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders a progress bar!
|
||||
**
|
||||
** Values:
|
||||
** ${heading}
|
||||
** [${percent}===___] ${value ?? percent}
|
||||
**
|
||||
** Slots:
|
||||
** ${heading}
|
||||
** ${bar} ${value}
|
||||
*******************************************************************************/
|
||||
export default function ProgressBarBlock({data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Typography component="div" variant="button" color="text" fontWeight="light" sx={{textTransform: "none"}}>
|
||||
{
|
||||
data.values.heading &&
|
||||
<div style={{marginBottom: "0.25rem", fontWeight: 500, color: "#3D3D3D"}}>
|
||||
<BlockElementWrapper data={data} slot="heading">
|
||||
<span>{data.values.heading}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{display: "flex", alignItems: "center", marginBottom: "0.75rem"}}>
|
||||
|
||||
<BlockElementWrapper data={data} slot="bar" linkProps={{style: {width: "100%"}}}>
|
||||
<div style={{background: "#E0E0E0", width: "100%", borderRadius: "0.5rem", height: "1rem"}}>
|
||||
{
|
||||
data.values.percent > 0 ? <div style={{background: data.styles.barColor ?? "#0062ff", minWidth: "1rem", width: `${data.values.percent}%`, borderRadius: "0.5rem", height: "1rem"}}></div> : <></>
|
||||
}
|
||||
</div>
|
||||
</BlockElementWrapper>
|
||||
|
||||
<div style={{width: "60px", textAlign: "right", fontWeight: 600, color: "#3D3D3D"}}>
|
||||
<BlockElementWrapper data={data} slot="value">
|
||||
<span>{data.values.value ?? `${(data.values.percent as number).toFixed(1)}%`}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Typography>);
|
||||
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders a label & value, meant to be used as a detail-row in a
|
||||
** sub-row within a table widget
|
||||
**
|
||||
** ${label} ${value}
|
||||
*******************************************************************************/
|
||||
export default function TableSubRowDetailRowBlock({data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<div style={{display: "flex", maxWidth: "calc(100% - 24px)", justifyContent: "space-between"}}>
|
||||
|
||||
{
|
||||
data.values.label &&
|
||||
<div style={{overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis"}}>
|
||||
<BlockElementWrapper data={data} slot="label">
|
||||
<span style={{color: data.styles.labelColor}}>{data.values.label}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
data.values.value &&
|
||||
<BlockElementWrapper data={data} slot="value">
|
||||
<span style={{color: data.styles.valueColor}}>{data.values.value}</span>
|
||||
</BlockElementWrapper>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... just some text.
|
||||
**
|
||||
** ${text}
|
||||
*******************************************************************************/
|
||||
export default function TextBlock({data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
return (
|
||||
<BlockElementWrapper data={data} slot="">
|
||||
<span>{data.values.text}</span>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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 Icon from "@mui/material/Icon";
|
||||
import React from "react";
|
||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders an up/down icon, a number, and some context
|
||||
**
|
||||
** ${icon} ${number} ${context}
|
||||
*
|
||||
** or, if style.isStacked:
|
||||
*
|
||||
** ${icon} ${number}
|
||||
** ${context}
|
||||
*******************************************************************************/
|
||||
export default function UpOrDownNumberBlock({data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
if (!data.styles)
|
||||
{
|
||||
data.styles = {};
|
||||
}
|
||||
|
||||
if (!data.values)
|
||||
{
|
||||
data.values = {};
|
||||
}
|
||||
|
||||
const UP_ICON = "arrow_drop_up";
|
||||
const DOWN_ICON = "arrow_drop_down";
|
||||
|
||||
const defaultGreenColor = "#2BA83F";
|
||||
const defaultRedColor = "#FB4141";
|
||||
|
||||
const goodOrBadColor = data.styles.colorOverride ?? (data.values.isGood ? defaultGreenColor : defaultRedColor);
|
||||
const iconName = data.values.isUp ? UP_ICON : DOWN_ICON;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
|
||||
|
||||
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
||||
<BlockElementWrapper data={data} slot="number">
|
||||
<>
|
||||
<Icon sx={{color: goodOrBadColor, alignSelf: "flex-end", fontSize: "2.25rem !important", lineHeight: "0.875rem", height: "1rem", width: "2rem",}}>{iconName}</Icon>
|
||||
<span style={{color: goodOrBadColor}}>{data.values.number}</span>
|
||||
</>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
|
||||
<div style={{fontWeight: 500, fontSize: "0.875rem", color: "#7b809a", marginLeft: "0.25rem"}}>
|
||||
<BlockElementWrapper data={data} slot="context">
|
||||
<span>{data.values.context}</span>
|
||||
</BlockElementWrapper>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -28,7 +28,6 @@ import {Bar} from "react-chartjs-2";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {chartColors, DefaultChartData} from "qqq/components/widgets/charts/DefaultChartData";
|
||||
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
@ -40,61 +39,18 @@ ChartJS.register(
|
||||
);
|
||||
|
||||
export const options = {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
elements: {
|
||||
bar: {
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
// todo - some configs around this
|
||||
callbacks: {
|
||||
title: function(context: any)
|
||||
{
|
||||
return ("");
|
||||
},
|
||||
label: function(context: any)
|
||||
{
|
||||
if(context.dataset.label.startsWith(context.label))
|
||||
{
|
||||
return `${context.label}: ${context.formattedValue}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
boxHeight: 6,
|
||||
boxWidth: 6,
|
||||
padding: 12,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: {display: false},
|
||||
grid: {offset: false},
|
||||
ticks: {autoSkip: false, maxRotation: 90}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
position: "right",
|
||||
ticks: {precision: 0}
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -102,12 +58,10 @@ export const options = {
|
||||
interface Props
|
||||
{
|
||||
data: DefaultChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const {gradients} = colors;
|
||||
|
||||
function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
function StackedBarChart({data}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -116,30 +70,23 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
|
||||
const handleClick = (e: Array<{}>) =>
|
||||
{
|
||||
if (e && e.length > 0 && data?.urls && data?.urls.length)
|
||||
if(e && e.length > 0 && data?.urls && data?.urls.length)
|
||||
{
|
||||
// @ts-ignore
|
||||
navigate(data.urls[e[0]["index"]]);
|
||||
}
|
||||
console.log(e);
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (data)
|
||||
if(data)
|
||||
{
|
||||
data?.datasets.forEach((dataset: any, index: number) =>
|
||||
{
|
||||
if (!dataset.backgroundColor)
|
||||
{
|
||||
if (gradients[chartColors[index]])
|
||||
{
|
||||
dataset.backgroundColor = gradients[chartColors[index]].state;
|
||||
}
|
||||
else
|
||||
{
|
||||
dataset.backgroundColor = chartColors[index];
|
||||
}
|
||||
dataset.backgroundColor = gradients[chartColors[index]].state;
|
||||
}
|
||||
});
|
||||
setStateData(stateData);
|
||||
@ -148,13 +95,8 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
|
||||
|
||||
|
||||
return data ? (
|
||||
<Box>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
<Box width="100%" height="300px">
|
||||
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;
|
||||
<Box p={3}><Bar data={data} options={options} getElementsAtEvent={handleClick} /></Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} /> ;
|
||||
}
|
||||
|
||||
export default StackedBarChart;
|
||||
|
@ -63,7 +63,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
@ -86,7 +86,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
@ -88,7 +88,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
@ -107,7 +107,7 @@ const options = {
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
|
@ -69,7 +69,7 @@ function configs(labels: any, datasets: any)
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
@ -90,7 +90,7 @@ function configs(labels: any, datasets: any)
|
||||
font: {
|
||||
size: 14,
|
||||
weight: 300,
|
||||
family: "SF Pro Display,Roboto",
|
||||
family: "Roboto",
|
||||
style: "normal",
|
||||
lineHeight: 2,
|
||||
},
|
||||
|
@ -30,7 +30,6 @@ import {useNavigate} from "react-router-dom";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {chartColors} from "qqq/components/widgets/charts/DefaultChartData";
|
||||
import configs from "qqq/components/widgets/charts/piechart/PieChartConfigs";
|
||||
import ChartSubheaderWithData, {ChartSubheaderData} from "qqq/components/widgets/components/ChartSubheaderWithData";
|
||||
|
||||
//////////////////////////////////////////
|
||||
// structure of expected bar chart data //
|
||||
@ -52,29 +51,25 @@ interface Props
|
||||
{
|
||||
description?: string;
|
||||
chartData: PieChartData;
|
||||
chartSubheaderData?: ChartSubheaderData;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Element
|
||||
function PieChart({description, chartData}: Props): JSX.Element
|
||||
{
|
||||
const navigate = useNavigate();
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
if (chartData && chartData.dataset)
|
||||
{
|
||||
if(!chartData.dataset.backgroundColors)
|
||||
{
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
chartData.dataset.backgroundColors = chartColors;
|
||||
}
|
||||
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (chartData)
|
||||
if(chartData)
|
||||
{
|
||||
setDataLoaded(true);
|
||||
}
|
||||
@ -82,51 +77,53 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
|
||||
|
||||
const handleClick = (e: Array<{}>) =>
|
||||
{
|
||||
if (e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
||||
if(e && e.length > 0 && chartData?.dataset?.urls && chartData?.dataset?.urls.length)
|
||||
{
|
||||
// @ts-ignore
|
||||
navigate(chartData.dataset.urls[e[0]["index"]]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1, border: 0}}>
|
||||
<Box>
|
||||
<Box>
|
||||
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
|
||||
</Box>
|
||||
<Box width="100%" height="300px">
|
||||
{useMemo(
|
||||
() => (
|
||||
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
),
|
||||
[chartData]
|
||||
)}
|
||||
</Box>
|
||||
{
|
||||
!chartData && (
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular" />
|
||||
<Card sx={{minHeight: "400px", boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
||||
<Box mt={3}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={12} justifyContent="center">
|
||||
<Box width="100%" height="80%" py={2} pr={2} pl={2}>
|
||||
{useMemo(
|
||||
() => (
|
||||
<Pie data={data} options={options} getElementsAtEvent={handleClick} />
|
||||
),
|
||||
[chartData]
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
{
|
||||
! chartData && (
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
top: "40%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
display: "flex",
|
||||
justifyContent: "center"}}>
|
||||
<Skeleton sx={{width: "150px", height: "150px"}} variant="circular"/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
{
|
||||
description && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
||||
<MDTypography variant="button" color="text" fontWeight="light">
|
||||
{parse(description)}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box pb={2} px={2} display="flex" flexDirection={{xs: "column", sm: "row"}} mt="auto">
|
||||
<MDTypography variant="button" color="text" fontWeight="light">
|
||||
{parse(description)}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
@ -30,16 +30,10 @@ function configs(labels: any, datasets: any)
|
||||
if (datasets.backgroundColors)
|
||||
{
|
||||
datasets.backgroundColors.forEach((color: string) =>
|
||||
{
|
||||
if (gradients[color])
|
||||
{
|
||||
backgroundColors.push(gradients[color].state);
|
||||
}
|
||||
else
|
||||
{
|
||||
backgroundColors.push(color);
|
||||
}
|
||||
});
|
||||
gradients[color]
|
||||
? backgroundColors.push(gradients[color].state)
|
||||
: backgroundColors.push(dark.main)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -64,49 +58,12 @@ function configs(labels: any, datasets: any)
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
maintainAspectRatio: true,
|
||||
responsive: true,
|
||||
aspectRatio: 2,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context: any)
|
||||
{
|
||||
let percentSuffix = "";
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// make percent by dividing this slice's value by the sum of all values //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
const thisSlice = context.dataset.data[context.dataIndex];
|
||||
const sum = context.dataset.data.reduce((acc: number, val: number) => acc + val, 0);
|
||||
percentSuffix = " (" + Number(100 * thisSlice / sum).toFixed(1) + "%)";
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// leave percentSuffix empty
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// our labels already have the value in them - so just use the label in the //
|
||||
// tooltip (lib by default puts label + value, so we were duplicating value!) //
|
||||
// oh, and we add percent if we can //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
return context.label + percentSuffix;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
pointStyle: "circle",
|
||||
padding: 12,
|
||||
boxHeight: 8,
|
||||
boxWidth: 8,
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
|
@ -1,105 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
export interface ChartSubheaderData
|
||||
{
|
||||
mainNumber: number;
|
||||
vsPreviousPercent: number;
|
||||
vsPreviousNumber: number;
|
||||
isUpVsPrevious: boolean;
|
||||
isGoodVsPrevious: boolean;
|
||||
vsDescription: string;
|
||||
mainNumberUrl: string;
|
||||
previousNumberUrl: string;
|
||||
}
|
||||
|
||||
interface Props
|
||||
{
|
||||
chartSubheaderData: ChartSubheaderData;
|
||||
}
|
||||
|
||||
const GOOD_COLOR = colors.success.main;
|
||||
const BAD_COLOR = colors.error.main;
|
||||
const UP_ICON = "arrow_drop_up";
|
||||
const DOWN_ICON = "arrow_drop_down";
|
||||
|
||||
function StackedBarChart({chartSubheaderData}: Props): JSX.Element
|
||||
{
|
||||
let color = "black";
|
||||
if (chartSubheaderData && chartSubheaderData.isGoodVsPrevious != null)
|
||||
{
|
||||
color = chartSubheaderData.isGoodVsPrevious ? GOOD_COLOR : BAD_COLOR;
|
||||
}
|
||||
|
||||
let iconName: string = null;
|
||||
if (chartSubheaderData && chartSubheaderData.isUpVsPrevious != null)
|
||||
{
|
||||
iconName = chartSubheaderData.isUpVsPrevious ? UP_ICON : DOWN_ICON;
|
||||
}
|
||||
|
||||
let mainNumberElement = <Typography variant="h3" display="inline">{ValueUtils.getFormattedNumber(chartSubheaderData.mainNumber)}</Typography>;
|
||||
if(chartSubheaderData.mainNumberUrl)
|
||||
{
|
||||
mainNumberElement = <Link to={chartSubheaderData.mainNumberUrl}>{mainNumberElement}</Link>
|
||||
}
|
||||
mainNumberElement = <Box pr={1}>{mainNumberElement}</Box>
|
||||
|
||||
let previousNumberElement = (
|
||||
<>
|
||||
<Typography display="block" variant="body2" sx={{color: colors.gray.main, fontSize: ".875rem", fontWeight: 500}}>
|
||||
{chartSubheaderData.vsDescription}
|
||||
{chartSubheaderData.vsPreviousNumber && (<> ({ValueUtils.getFormattedNumber(chartSubheaderData.vsPreviousNumber)})</>)}
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
|
||||
if(chartSubheaderData.previousNumberUrl)
|
||||
{
|
||||
previousNumberElement = <Link to={chartSubheaderData.previousNumberUrl}>{previousNumberElement}</Link>
|
||||
}
|
||||
|
||||
return chartSubheaderData ? (
|
||||
<Box display="inline-flex" alignItems="flex-end" flexWrap="wrap">
|
||||
{mainNumberElement}
|
||||
{
|
||||
chartSubheaderData.vsPreviousPercent != null && iconName != null && (
|
||||
<Box display="inline-flex" alignItems="baseline" pb={0.5} ml={-0.5}>
|
||||
<Icon fontSize="medium" sx={{color: color, alignSelf: "flex-end"}}>{iconName}</Icon>
|
||||
<Typography display="inline" variant="body2" sx={{color: color, fontSize: ".875rem", fontWeight: 500}}>{chartSubheaderData.vsPreviousPercent}%</Typography>
|
||||
{previousNumberElement}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "12px"}} />;
|
||||
}
|
||||
|
||||
export default StackedBarChart;
|
179
src/qqq/components/widgets/components/DropdownMenu.tsx
Normal file
179
src/qqq/components/widgets/components/DropdownMenu.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {Collapse, Theme} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {SxProps} from "@mui/system";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
|
||||
export interface DropdownOption
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// inputs and defaults //
|
||||
/////////////////////////
|
||||
interface Props
|
||||
{
|
||||
name: string;
|
||||
defaultValue?: any;
|
||||
label?: string;
|
||||
dropdownOptions?: DropdownOption[];
|
||||
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface StartAndEndDate
|
||||
{
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
||||
{
|
||||
const customTimeValues: StartAndEndDate = {};
|
||||
if(defaultValue && defaultValue.id)
|
||||
{
|
||||
var parts = defaultValue.id.split(",");
|
||||
if(parts.length >= 2)
|
||||
{
|
||||
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
||||
}
|
||||
if(parts.length >= 3)
|
||||
{
|
||||
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return (customTimeValues);
|
||||
}
|
||||
|
||||
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
||||
{
|
||||
const backendTimeValues: StartAndEndDate = {};
|
||||
if(frontendDefaultValues && frontendDefaultValues.startDate)
|
||||
{
|
||||
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
||||
}
|
||||
if(frontendDefaultValues && frontendDefaultValues.endDate)
|
||||
{
|
||||
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
||||
}
|
||||
return (backendTimeValues);
|
||||
}
|
||||
|
||||
function DropdownMenu({name, defaultValue, label, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
{
|
||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
||||
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
||||
|
||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom"
|
||||
setCustomTimesVisible(isTimeframeCustom);
|
||||
|
||||
if(isTimeframeCustom)
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
onChangeCallback(label, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
||||
{
|
||||
if(customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
||||
{
|
||||
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
||||
}
|
||||
}
|
||||
|
||||
let customTimes = <></>;
|
||||
if (name == "timeframe")
|
||||
{
|
||||
const handleSubmit = async (values: any, actions: any) =>
|
||||
{
|
||||
};
|
||||
|
||||
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
||||
{
|
||||
customTimeValuesFrontend[fieldName] = event.target.value;
|
||||
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
const newDebounceTimeout = setTimeout(() =>
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}, 500);
|
||||
setDebounceTimeout(newDebounceTimeout);
|
||||
};
|
||||
|
||||
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
||||
<Collapse orientation="horizontal" in={customTimesVisible}>
|
||||
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
||||
{({}) => (
|
||||
<Form id="timeframe-form" autoComplete="off">
|
||||
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
||||
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Collapse>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<span style={{whiteSpace: "nowrap", display: "flex"}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
defaultValue={defaultValue}
|
||||
size="small"
|
||||
disablePortal
|
||||
id={`${label}-combo-box`}
|
||||
options={dropdownOptions}
|
||||
sx={{...sx, cursor: "pointer", display: "inline-block"}}
|
||||
onChange={handleOnChange}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
renderInput={(params: any) => <TextField {...params} label={label} />}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal"}}>{option.label}</li>
|
||||
)}
|
||||
/>
|
||||
{customTimes}
|
||||
</span>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownMenu;
|
@ -1,335 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {Collapse, Theme, InputAdornment} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {SxProps} from "@mui/system";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
|
||||
export interface DropdownOption
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// inputs and defaults //
|
||||
/////////////////////////
|
||||
interface Props
|
||||
{
|
||||
name: string;
|
||||
defaultValue?: any;
|
||||
label?: string;
|
||||
startIcon?: string;
|
||||
width?: number;
|
||||
disableClearable?: boolean;
|
||||
allowBackAndForth?: boolean;
|
||||
backAndForthInverted?: boolean;
|
||||
dropdownOptions?: DropdownOption[];
|
||||
onChangeCallback?: (dropdownLabel: string, data: any) => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
interface StartAndEndDate
|
||||
{
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
function parseCustomTimeValuesFromDefaultValue(defaultValue: any): StartAndEndDate
|
||||
{
|
||||
const customTimeValues: StartAndEndDate = {};
|
||||
if (defaultValue && defaultValue.id)
|
||||
{
|
||||
var parts = defaultValue.id.split(",");
|
||||
if (parts.length >= 2)
|
||||
{
|
||||
customTimeValues["startDate"] = ValueUtils.formatDateTimeValueForForm(parts[1]);
|
||||
}
|
||||
if (parts.length >= 3)
|
||||
{
|
||||
customTimeValues["endDate"] = ValueUtils.formatDateTimeValueForForm(parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return (customTimeValues);
|
||||
}
|
||||
|
||||
function makeBackendValuesFromFrontendValues(frontendDefaultValues: StartAndEndDate): StartAndEndDate
|
||||
{
|
||||
const backendTimeValues: StartAndEndDate = {};
|
||||
if (frontendDefaultValues && frontendDefaultValues.startDate)
|
||||
{
|
||||
backendTimeValues.startDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.startDate);
|
||||
}
|
||||
if (frontendDefaultValues && frontendDefaultValues.endDate)
|
||||
{
|
||||
backendTimeValues.endDate = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(frontendDefaultValues.endDate);
|
||||
}
|
||||
return (backendTimeValues);
|
||||
}
|
||||
|
||||
function WidgetDropdownMenu({name, defaultValue, label, startIcon, width, disableClearable, allowBackAndForth, backAndForthInverted, dropdownOptions, onChangeCallback, sx}: Props): JSX.Element
|
||||
{
|
||||
const [customTimesVisible, setCustomTimesVisible] = useState(defaultValue && defaultValue.id && defaultValue.id.startsWith("custom,"));
|
||||
const [customTimeValuesFrontend, setCustomTimeValuesFrontend] = useState(parseCustomTimeValuesFromDefaultValue(defaultValue) as StartAndEndDate);
|
||||
const [customTimeValuesBackend, setCustomTimeValuesBackend] = useState(makeBackendValuesFromFrontendValues(customTimeValuesFrontend) as StartAndEndDate);
|
||||
const [debounceTimeout, setDebounceTimeout] = useState(null as any);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [backDisabled, setBackDisabled] = useState(false);
|
||||
const [forthDisabled, setForthDisabled] = useState(false);
|
||||
|
||||
const doForceOpen = (event: React.MouseEvent<HTMLDivElement>) =>
|
||||
{
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
function getSelectedIndex(value: DropdownOption)
|
||||
{
|
||||
let currentIndex = null;
|
||||
for (let i = 0; i < dropdownOptions.length; i++)
|
||||
{
|
||||
if (value && dropdownOptions[i].id == value.id)
|
||||
{
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
const navigateBackAndForth = (event: React.MouseEvent, direction: -1 | 1) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
let currentIndex = getSelectedIndex(value);
|
||||
|
||||
if (currentIndex == null)
|
||||
{
|
||||
console.log("No current value.... TODO");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex == 0 && direction == -1)
|
||||
{
|
||||
console.log("Can't go -1");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndex == dropdownOptions.length - 1 && direction == 1)
|
||||
{
|
||||
console.log("Can't go +1");
|
||||
return;
|
||||
}
|
||||
|
||||
handleOnChange(event, dropdownOptions[currentIndex + direction], "navigatedBackAndForth");
|
||||
};
|
||||
|
||||
|
||||
const handleOnChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
setValue(newValue);
|
||||
|
||||
const isTimeframeCustom = name == "timeframe" && newValue && newValue.id == "custom";
|
||||
setCustomTimesVisible(isTimeframeCustom);
|
||||
|
||||
if (isTimeframeCustom)
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
onChangeCallback(label, newValue);
|
||||
}
|
||||
|
||||
/* this had bugs (seemed to not take immediate effect?), so don't use for now.
|
||||
let currentIndex = getSelectedIndex(value);
|
||||
if(currentIndex == 0)
|
||||
{
|
||||
backAndForthInverted ? setForthDisabled(true) : setBackDisabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
backAndForthInverted ? setForthDisabled(false) : setBackDisabled(false);
|
||||
}
|
||||
|
||||
if (currentIndex == dropdownOptions.length - 1)
|
||||
{
|
||||
backAndForthInverted ? setBackDisabled(true) : setForthDisabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
backAndForthInverted ? setBackDisabled(false) : setForthDisabled(false);
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
const handleOnInputChange = (event: any, newValue: any, reason: string) =>
|
||||
{
|
||||
setInputValue(newValue);
|
||||
};
|
||||
|
||||
const callOnChangeCallbackIfCustomTimeframeHasDateValues = () =>
|
||||
{
|
||||
if (customTimeValuesBackend["startDate"] && customTimeValuesBackend["endDate"])
|
||||
{
|
||||
onChangeCallback(label, {id: `custom,${customTimeValuesBackend["startDate"]},${customTimeValuesBackend["endDate"]}`, label: "Custom"});
|
||||
}
|
||||
};
|
||||
|
||||
let customTimes = <></>;
|
||||
if (name == "timeframe")
|
||||
{
|
||||
const handleSubmit = async (values: any, actions: any) =>
|
||||
{
|
||||
};
|
||||
|
||||
const dateChanged = (fieldName: "startDate" | "endDate", event: any) =>
|
||||
{
|
||||
customTimeValuesFrontend[fieldName] = event.target.value;
|
||||
customTimeValuesBackend[fieldName] = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(event.target.value);
|
||||
|
||||
clearTimeout(debounceTimeout);
|
||||
const newDebounceTimeout = setTimeout(() =>
|
||||
{
|
||||
callOnChangeCallbackIfCustomTimeframeHasDateValues();
|
||||
}, 500);
|
||||
setDebounceTimeout(newDebounceTimeout);
|
||||
};
|
||||
|
||||
customTimes = <Box sx={{display: "inline-block", position: "relative", top: "-7px"}}>
|
||||
<Collapse orientation="horizontal" in={customTimesVisible}>
|
||||
<Formik initialValues={customTimeValuesFrontend} onSubmit={handleSubmit}>
|
||||
{({}) => (
|
||||
<Form id="timeframe-form" autoComplete="off">
|
||||
<Field name="startDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe Start" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("startDate", event)} />
|
||||
<Field name="endDate" type="datetime-local" as={MDInput} variant="standard" label="Custom Timeframe End" InputLabelProps={{shrink: true}} InputProps={{size: "small"}} sx={{ml: 2, width: 198}} onChange={(event: any) => dateChanged("endDate", event)} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Collapse>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
const startAdornment = startIcon ? <Icon sx={{fontSize: "1.25rem!important", color: colors.gray.main, paddingLeft: allowBackAndForth ? "auto" : "0.25rem", width: allowBackAndForth ? "1.5rem" : "1.75rem"}}>{startIcon}</Icon> : null;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we tried this end-adornment, for a different style of down-arrow - but by using it, we then messed something else up (i forget what), so... not used right now //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const endAdornment = <InputAdornment position="end" sx={{position: "absolute", right: allowBackAndForth ? "-0.5rem" : "0.5rem"}}><Icon sx={{fontSize: "1.75rem!important", color: colors.gray.main}}>keyboard_arrow_down</Icon></InputAdornment>;
|
||||
|
||||
const fontSize = "1rem";
|
||||
let optionPaddingLeftRems = 0.75;
|
||||
if(startIcon)
|
||||
{
|
||||
optionPaddingLeftRems += allowBackAndForth ? 1.5 : 1.75
|
||||
}
|
||||
if(allowBackAndForth)
|
||||
{
|
||||
optionPaddingLeftRems += 2.5;
|
||||
}
|
||||
|
||||
return (
|
||||
dropdownOptions ? (
|
||||
<Box sx={{whiteSpace: "nowrap", display: "flex",
|
||||
"& .MuiPopperUnstyled-root": {
|
||||
border: `1px solid ${colors.grayLines.main}`,
|
||||
borderTop: "none",
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
padding: 0,
|
||||
}, "& .MuiPaper-rounded": {
|
||||
borderRadius: "0 0 0.75rem 0.75rem",
|
||||
}
|
||||
}} className="dashboardDropdownMenu">
|
||||
<Autocomplete
|
||||
id={`${label}-combo-box`}
|
||||
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={handleOnInputChange}
|
||||
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
|
||||
open={isOpen}
|
||||
onOpen={() => setIsOpen(true)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
|
||||
size="small"
|
||||
disablePortal
|
||||
disableClearable={disableClearable}
|
||||
options={dropdownOptions}
|
||||
sx={{
|
||||
...sx,
|
||||
cursor: "pointer",
|
||||
display: "inline-block",
|
||||
"& .MuiOutlinedInput-notchedOutline": {
|
||||
border: "none"
|
||||
},
|
||||
}}
|
||||
renderInput={(params: any) =>
|
||||
<>
|
||||
<Box sx={{width: `${width}px`, background: "white", borderRadius: isOpen ? "0.75rem 0.75rem 0 0" : "0.75rem", border: `1px solid ${colors.grayLines.main}`, "& *": {cursor: "pointer"}}} display="flex" alignItems="center" onClick={(event) => doForceOpen(event)}>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? 1 : -1)} disabled={backDisabled}><Icon>navigate_before</Icon></IconButton>}
|
||||
<TextField {...params} placeholder={label} sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontSize: fontSize
|
||||
}
|
||||
}} InputProps={{...params.InputProps, startAdornment: startAdornment/*, endAdornment: endAdornment*/}}
|
||||
/>
|
||||
{allowBackAndForth && <IconButton onClick={(event) => navigateBackAndForth(event, backAndForthInverted ? -1 : 1)} disabled={forthDisabled}><Icon>navigate_next</Icon></IconButton>}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
renderOption={(props, option: DropdownOption) => (
|
||||
<li {...props} style={{whiteSpace: "normal", fontSize: fontSize, paddingLeft: `${optionPaddingLeftRems}rem`}}>{option.label}</li>
|
||||
)}
|
||||
|
||||
noOptionsText={<Box fontSize={fontSize}>No options found</Box>}
|
||||
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
width: `${width}px!important`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{customTimes}
|
||||
</Box>
|
||||
) : null
|
||||
);
|
||||
}
|
||||
|
||||
export default WidgetDropdownMenu;
|
@ -286,15 +286,18 @@ export default function DataBagViewer({dataBagId}: Props): JSX.Element
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1, mt: -3}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
</Tabs>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Raw Data" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "150px"}} />
|
||||
<Tab label="Data Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "150px"}} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
|
@ -22,15 +22,10 @@
|
||||
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 Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {DataGridPro, GridCallbackDetails, GridEventListener, GridFilterModel, gridPreferencePanelStateSelector, GridRowParams, GridSelectionModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useNavigate, Link} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget";
|
||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
@ -48,15 +43,10 @@ const qController = Client.getInstance();
|
||||
|
||||
function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
{
|
||||
const instance = useRef({timer: null});
|
||||
const [rows, setRows] = useState([]);
|
||||
const [records, setRecords] = useState([] as QRecord[])
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [allColumns, setAllColumns] = useState([])
|
||||
const [csv, setCsv] = useState(null as string);
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() =>
|
||||
@ -85,7 +75,6 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const allColumns = [... columns];
|
||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
@ -106,42 +95,39 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
setRows(rows);
|
||||
setRecords(records)
|
||||
setColumns(columns);
|
||||
|
||||
let csv = "";
|
||||
for (let i = 0; i < allColumns.length; i++)
|
||||
{
|
||||
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
|
||||
for (let i = 0; i < records.length; i++)
|
||||
{
|
||||
for (let j = 0; j < allColumns.length; j++)
|
||||
{
|
||||
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
||||
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
|
||||
setCsv(csv);
|
||||
setFileName(fileName);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const exportCallback = () =>
|
||||
{
|
||||
let csv = "";
|
||||
for (let i = 0; i < allColumns.length; i++)
|
||||
{
|
||||
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
|
||||
for (let i = 0; i < records.length; i++)
|
||||
{
|
||||
for (let j = 0; j < allColumns.length; j++)
|
||||
{
|
||||
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
||||
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
|
||||
///////////////////
|
||||
// view all link //
|
||||
///////////////////
|
||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||
const labelAdditionalComponentsLeft: LabelComponent[] = []
|
||||
if(data && data.viewAllLink)
|
||||
{
|
||||
labelAdditionalElementsLeft.push(
|
||||
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
|
||||
<Link to={data.viewAllLink}>View All</Link>
|
||||
</Typography>
|
||||
)
|
||||
labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink));
|
||||
}
|
||||
|
||||
///////////////////
|
||||
@ -163,26 +149,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
const onExportClick = () =>
|
||||
{
|
||||
if(csv)
|
||||
{
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert("There is no data available to export.")
|
||||
}
|
||||
}
|
||||
|
||||
if(widgetMetaData?.showExportButton)
|
||||
{
|
||||
labelAdditionalElementsLeft.push(
|
||||
<Typography key={1} variant="body2" px={0} display="inline" position="relative">
|
||||
<Tooltip title={tooltipTitle}><Button sx={{px: 1, py: 0, minWidth: "initial"}} onClick={onExportClick} disabled={isExportDisabled}><Icon sx={{color: "#757575", fontSize: 1.25}}>save_alt</Icon></Button></Tooltip>
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle))
|
||||
|
||||
////////////////////
|
||||
// add new button //
|
||||
@ -199,102 +166,61 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// if a grid preference window is open, ignore and reset timer //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
const qInstance = await qController.loadMetaData()
|
||||
let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name)
|
||||
const tablePath = qInstance.getTablePathByName(data.childTableMetaData.name)
|
||||
if(tablePath)
|
||||
{
|
||||
tablePath = `${tablePath}/${params.id}`;
|
||||
DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance);
|
||||
navigate(`${tablePath}/${params.id}`);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
** So that we can useGridApiContext to add event handlers for mouse down and
|
||||
** row double-click (to make it so you don't accidentally click into records),
|
||||
** we have to define a grid component, so even though we don't want a custom
|
||||
** toolbar, that's why we have this (and why it returns empty)
|
||||
*******************************************************************************/
|
||||
function CustomToolbar()
|
||||
{
|
||||
const handleMouseDown: GridEventListener<"cellMouseDown"> = ( params, event, details ) =>
|
||||
{
|
||||
setGridMouseDownX(event.clientX);
|
||||
setGridMouseDownY(event.clientY);
|
||||
clearTimeout(instance.current.timer);
|
||||
};
|
||||
|
||||
const handleDoubleClick: GridEventListener<"rowDoubleClick"> = (event: any) =>
|
||||
{
|
||||
clearTimeout(instance.current.timer);
|
||||
};
|
||||
|
||||
const apiRef = useGridApiContext();
|
||||
useGridApiEventHandler(apiRef, "cellMouseDown", handleMouseDown);
|
||||
useGridApiEventHandler(apiRef, "rowDoubleClick", handleDoubleClick);
|
||||
|
||||
return (<GridToolbarContainer />);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Widget
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={data}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||
>
|
||||
<Box mx={-2} mb={-3}>
|
||||
<DataGridPro
|
||||
autoHeight
|
||||
sx={{
|
||||
borderBottom: "none",
|
||||
borderLeft: "none",
|
||||
borderRight: "none"
|
||||
}}
|
||||
rows={rows}
|
||||
disableSelectionOnClick
|
||||
columns={columns}
|
||||
rowBuffer={10}
|
||||
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
|
||||
onRowClick={handleRowClick}
|
||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||
components={{
|
||||
Toolbar: CustomToolbar
|
||||
}}
|
||||
// pinnedColumns={pinnedColumns}
|
||||
// onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||
// pagination
|
||||
// paginationMode="server"
|
||||
// rowsPerPageOptions={[20]}
|
||||
// sortingMode="server"
|
||||
// filterMode="server"
|
||||
// page={pageNumber}
|
||||
// checkboxSelection
|
||||
rowCount={data && data.totalRows}
|
||||
// onPageSizeChange={handleRowsPerPageChange}
|
||||
// onStateChange={handleStateChange}
|
||||
// density={density}
|
||||
// loading={loading}
|
||||
// filterModel={filterModel}
|
||||
// onFilterModelChange={handleFilterChange}
|
||||
// columnVisibilityModel={columnVisibilityModel}
|
||||
// onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
// onColumnOrderChange={handleColumnOrderChange}
|
||||
// onSelectionModelChange={selectionChanged}
|
||||
// onSortModelChange={handleSortChange}
|
||||
// sortingOrder={[ "asc", "desc" ]}
|
||||
// sortModel={columnSortModel}
|
||||
/>
|
||||
</Box>
|
||||
<DataGridPro
|
||||
autoHeight
|
||||
rows={rows}
|
||||
disableSelectionOnClick
|
||||
columns={columns}
|
||||
rowBuffer={10}
|
||||
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
|
||||
onRowClick={handleRowClick}
|
||||
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
|
||||
// components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
||||
// pinnedColumns={pinnedColumns}
|
||||
// onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||
// pagination
|
||||
// paginationMode="server"
|
||||
// rowsPerPageOptions={[20]}
|
||||
// sortingMode="server"
|
||||
// filterMode="server"
|
||||
// page={pageNumber}
|
||||
// checkboxSelection
|
||||
rowCount={data && data.totalRows}
|
||||
// onPageSizeChange={handleRowsPerPageChange}
|
||||
// onStateChange={handleStateChange}
|
||||
// density={density}
|
||||
// loading={loading}
|
||||
// filterModel={filterModel}
|
||||
// onFilterModelChange={handleFilterChange}
|
||||
// columnVisibilityModel={columnVisibilityModel}
|
||||
// onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
// onColumnOrderChange={handleColumnOrderChange}
|
||||
// onSelectionModelChange={selectionChanged}
|
||||
// onSortModelChange={handleSortChange}
|
||||
// sortingOrder={[ "asc", "desc" ]}
|
||||
// sortModel={columnSortModel}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
@ -392,8 +392,14 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
return JSON.stringify(new QQueryFilter([new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, [scriptRevisionId])]));
|
||||
}
|
||||
|
||||
/*
|
||||
position: relative;
|
||||
left: -356px;
|
||||
width: calc(100% + 380px);
|
||||
*/
|
||||
|
||||
return (
|
||||
<Grid container className="scriptViewer" m={-2} pt={4} width={"calc(100% + 2rem)"}>
|
||||
<Grid container className="scriptViewer">
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
{
|
||||
@ -424,17 +430,20 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1, mt: -3}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" />
|
||||
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" />
|
||||
</Tabs>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mt={-6}>
|
||||
<Typography variant="h5" p={2}></Typography>
|
||||
<Tabs
|
||||
sx={{m: 1}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "100px"}} />
|
||||
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "100px"}} />
|
||||
<Tab label="Test" id="simple-tab-1" aria-controls="simple-tabpanel-2" sx={{width: "100px"}} />
|
||||
<Tab label="Docs" id="simple-tab-1" aria-controls="simple-tabpanel-3" sx={{width: "100px"}} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
@ -489,7 +498,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
||||
editorProps={{$blockScrolling: true}}
|
||||
setOptions={{useWorker: false}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
height="368px"
|
||||
value={getSelectedFileCode()}
|
||||
style={{borderTop: "1px solid lightgray", borderBottomRightRadius: "1rem"}}
|
||||
/>
|
||||
|
@ -18,7 +18,6 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {tooltipClasses, TooltipProps} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
@ -31,18 +30,15 @@ import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import parse from "html-react-parser";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable, useExpanded} from "react-table";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {useAsyncDebounce, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
|
||||
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
|
||||
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
||||
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
|
||||
import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
|
||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -51,8 +47,6 @@ interface Props
|
||||
canSearch?: boolean;
|
||||
showTotalEntries?: boolean;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
table: TableDataInput;
|
||||
pagination?: {
|
||||
variant: "contained" | "gradient";
|
||||
@ -60,21 +54,8 @@ interface Props
|
||||
};
|
||||
isSorted?: boolean;
|
||||
noEndBorder?: boolean;
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
}
|
||||
|
||||
DataTable.defaultProps = {
|
||||
entriesPerPage: 10,
|
||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||
canSearch: false,
|
||||
showTotalEntries: true,
|
||||
fixedStickyLastRow: false,
|
||||
fixedHeight: null,
|
||||
pagination: {variant: "gradient", color: "info"},
|
||||
isSorted: true,
|
||||
noEndBorder: false,
|
||||
};
|
||||
|
||||
const NoMaxWidthTooltip = styled(({className, ...props}: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{popper: className}} />
|
||||
))({
|
||||
@ -90,13 +71,10 @@ function DataTable({
|
||||
hidePaginationDropdown,
|
||||
canSearch,
|
||||
showTotalEntries,
|
||||
fixedStickyLastRow,
|
||||
fixedHeight,
|
||||
table,
|
||||
pagination,
|
||||
isSorted,
|
||||
noEndBorder,
|
||||
widgetMetaData
|
||||
}: Props): JSX.Element
|
||||
{
|
||||
let defaultValue: any;
|
||||
@ -105,77 +83,8 @@ function DataTable({
|
||||
defaultValue = (entriesPerPage) ? entriesPerPage : "10";
|
||||
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
||||
|
||||
let widths = [];
|
||||
for(let i = 0; i<table.columns.length; i++)
|
||||
{
|
||||
const column = table.columns[i];
|
||||
if(column.type !== "hidden")
|
||||
{
|
||||
widths.push(table.columns[i].width ?? "1fr");
|
||||
}
|
||||
}
|
||||
|
||||
let showExpandColumn = false;
|
||||
if(table.rows)
|
||||
{
|
||||
for (let i = 0; i < table.rows.length; i++)
|
||||
{
|
||||
if (table.rows[i].subRows)
|
||||
{
|
||||
showExpandColumn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columnsToMemo = [...table.columns];
|
||||
if(showExpandColumn)
|
||||
{
|
||||
widths.push("60px");
|
||||
columnsToMemo.push(
|
||||
{
|
||||
///////////////////////////////
|
||||
// Build our expander column //
|
||||
///////////////////////////////
|
||||
id: "__expander",
|
||||
width: 60,
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// use this block if we want to do expand-all //
|
||||
////////////////////////////////////////////////
|
||||
// @ts-ignore
|
||||
// header: ({getToggleAllRowsExpandedProps, isAllRowsExpanded}) => (
|
||||
// <span {...getToggleAllRowsExpandedProps()}>
|
||||
// {isAllRowsExpanded ? "yes" : "no"}
|
||||
// </span>
|
||||
// ),
|
||||
header: () => (<span />),
|
||||
|
||||
// @ts-ignore
|
||||
cell: ({row}) =>
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter to build the toggle for expanding a row //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We could use the row.depth property and paddingLeft to indicate the depth of the row //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// style: {paddingLeft: `${row.depth * 2}rem`,},
|
||||
})}
|
||||
>
|
||||
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const columns = useMemo<any>(() => columnsToMemo, [table]);
|
||||
const columns = useMemo<any>(() => table.columns, [table]);
|
||||
const data = useMemo<any>(() => table.rows, [table]);
|
||||
const gridTemplateColumns = widths.join(" ");
|
||||
|
||||
if (!columns || !data)
|
||||
{
|
||||
@ -186,7 +95,6 @@ function DataTable({
|
||||
{columns, data, initialState: {pageIndex: 0}},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
usePagination
|
||||
);
|
||||
|
||||
@ -205,7 +113,7 @@ function DataTable({
|
||||
previousPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: {pageIndex, pageSize, globalFilter, expanded},
|
||||
state: {pageIndex, pageSize, globalFilter},
|
||||
}: any = tableInstance;
|
||||
|
||||
// Set the default value for the entries per page when component mounts
|
||||
@ -285,170 +193,11 @@ function DataTable({
|
||||
entriesEnd = pageSize * (pageIndex + 1);
|
||||
}
|
||||
|
||||
let visibleFooterRows = 1;
|
||||
if(expanded && expanded[`${table.rows.length-1}`])
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// todo - should count how many are expanded... //
|
||||
//////////////////////////////////////////////////
|
||||
visibleFooterRows = 2;
|
||||
}
|
||||
|
||||
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
||||
{
|
||||
let boxStyle = {};
|
||||
if(fixedStickyLastRow)
|
||||
{
|
||||
boxStyle = isFooter
|
||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
||||
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||
}
|
||||
|
||||
let innerBoxStyle = {};
|
||||
if(fixedStickyLastRow && isFooter)
|
||||
{
|
||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
||||
}
|
||||
|
||||
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
||||
<Table {...getTableProps()}>
|
||||
{
|
||||
includeHead && (
|
||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
|
||||
let overrideNoEndBorder = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// don't do an end-border on nested rows - unless they're the last one in a set //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if(row.depth > 0)
|
||||
{
|
||||
overrideNoEndBorder = true;
|
||||
if(key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||
{
|
||||
overrideNoEndBorder = false;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////
|
||||
// don't do end-border on the footer //
|
||||
///////////////////////////////////////
|
||||
if(isFooter)
|
||||
{
|
||||
overrideNoEndBorder = true;
|
||||
}
|
||||
|
||||
let background = "initial";
|
||||
if(isFooter)
|
||||
{
|
||||
background = "#EEEEEE";
|
||||
}
|
||||
else if(row.depth > 0 || row.isExpanded)
|
||||
{
|
||||
background = "#FAFAFA";
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||
depth={row.depth}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "composite" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "block" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(cell.column.id === "__expander") && cell.render("cell")
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box></Box>
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && ! hidePaginationDropdown) || canSearch) ? (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || ! hidePaginationDropdown) && (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Autocomplete
|
||||
disableClearable
|
||||
@ -456,7 +205,7 @@ function DataTable({
|
||||
options={entries}
|
||||
onChange={(event, newValues: any) =>
|
||||
{
|
||||
if (typeof newValues === "string")
|
||||
if(typeof newValues === "string")
|
||||
{
|
||||
setEntriesPerPage(parseInt(newValues, 10));
|
||||
}
|
||||
@ -491,17 +240,82 @@ function DataTable({
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
<Table {...getTableProps()}>
|
||||
<Box component="thead">
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
width={column.width ? column.width : "auto"}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{page.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top"}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
noBorder={noEndBorder && rows.length - 1 === key}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" flexDirection="column" height="100%">
|
||||
{
|
||||
fixedStickyLastRow ? (
|
||||
<>
|
||||
{getTable(true, page.slice(0, page.length - visibleFooterRows), false)}
|
||||
{getTable(false, page.slice(page.length - visibleFooterRows), true)}
|
||||
</>
|
||||
) : getTable(true, page, false)
|
||||
}
|
||||
</Box>
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell>{parse(cell.value)}</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
@ -554,4 +368,15 @@ function DataTable({
|
||||
);
|
||||
}
|
||||
|
||||
// Declaring default props for DataTable
|
||||
DataTable.defaultProps = {
|
||||
entriesPerPage: 10,
|
||||
entriesPerPageOptions: ["5", "10", "15", "20", "25"],
|
||||
canSearch: false,
|
||||
showTotalEntries: true,
|
||||
pagination: {variant: "gradient", color: "info"},
|
||||
isSorted: true,
|
||||
noEndBorder: false,
|
||||
};
|
||||
|
||||
export default DataTable;
|
||||
|
@ -20,7 +20,6 @@
|
||||
*/
|
||||
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Table from "@mui/material/Table";
|
||||
@ -55,14 +54,11 @@ interface Props
|
||||
noRowsFoundHTML?: string;
|
||||
rowsPerPage?: number;
|
||||
hidePaginationDropdown?: boolean;
|
||||
fixedStickyLastRow?: boolean;
|
||||
fixedHeight?: number;
|
||||
data: TableDataInput;
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element
|
||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown}: Props): JSX.Element
|
||||
{
|
||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||
|
||||
@ -76,21 +72,19 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box className="tableCard" mx={-2} mb="-28px" pt="11px" pb="0.25rem">
|
||||
<Box py={1}>
|
||||
{
|
||||
data && data.columns && !noRowsFoundHTML ?
|
||||
<DataTable
|
||||
table={data}
|
||||
entriesPerPage={rowsPerPage}
|
||||
hidePaginationDropdown={hidePaginationDropdown}
|
||||
fixedStickyLastRow={fixedStickyLastRow}
|
||||
fixedHeight={fixedHeight}
|
||||
showTotalEntries={false}
|
||||
isSorted={false}
|
||||
widgetMetaData={widgetMetaData}
|
||||
noEndBorder
|
||||
/>
|
||||
: noRowsFoundHTML ?
|
||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
||||
<Box p={3} pt={1} pb={1} sx={{textAlign: "center"}}>
|
||||
<MDTypography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
@ -120,7 +114,7 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
||||
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
||||
{Array(8).fill(0).map((_, j) =>
|
||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||
<DefaultCell><Skeleton /></DefaultCell>
|
||||
</DataTableBodyCell>
|
||||
)}
|
||||
</TableRow>
|
||||
|
@ -25,8 +25,7 @@ import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Q
|
||||
import {htmlToText} from "html-to-text";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||
import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
||||
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
@ -44,8 +43,6 @@ TableWidget.defaultProps = {
|
||||
function TableWidget(props: Props): JSX.Element
|
||||
{
|
||||
const [isExportDisabled, setIsExportDisabled] = useState(false); // hmm, would like true here, but it broke...
|
||||
const [csv, setCsv] = useState(null as string);
|
||||
const [fileName, setFileName] = useState(null as string);
|
||||
|
||||
const rows = props.widgetData?.rows;
|
||||
const columns = props.widgetData?.columns;
|
||||
@ -59,8 +56,14 @@ function TableWidget(props: Props): JSX.Element
|
||||
}
|
||||
setIsExportDisabled(isExportDisabled);
|
||||
|
||||
}, [props.widgetMetaData, props.widgetData]);
|
||||
|
||||
const exportCallback = () =>
|
||||
{
|
||||
if (props.widgetData && rows && columns)
|
||||
{
|
||||
console.log(props.widgetData);
|
||||
|
||||
let csv = "";
|
||||
for (let j = 0; j < columns.length; j++)
|
||||
{
|
||||
@ -82,56 +85,29 @@ function TableWidget(props: Props): JSX.Element
|
||||
}
|
||||
|
||||
const cell = rows[i][columns[j].accessor];
|
||||
let text = cell;
|
||||
if(columns[j].type != "default")
|
||||
{
|
||||
text = htmlToText(cell,
|
||||
{
|
||||
selectors: [
|
||||
{selector: "a", format: "inline"},
|
||||
{selector: ".MuiIcon-root", format: "skip"},
|
||||
{selector: ".button", format: "skip"}
|
||||
]
|
||||
});
|
||||
}
|
||||
const text = htmlToText(cell,
|
||||
{
|
||||
selectors: [
|
||||
{selector: "a", format: "inline"},
|
||||
{selector: ".MuiIcon-root", format: "skip"},
|
||||
{selector: ".button", format: "skip"}
|
||||
]
|
||||
});
|
||||
csv += `"${ValueUtils.cleanForCsv(text)}"`;
|
||||
}
|
||||
csv += "\n";
|
||||
}
|
||||
|
||||
setCsv(csv);
|
||||
console.log(csv);
|
||||
|
||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
||||
setFileName(fileName)
|
||||
|
||||
console.log(`useEffect, setting fileName ${fileName}`);
|
||||
}
|
||||
|
||||
}, [props.widgetMetaData, props.widgetData]);
|
||||
|
||||
const onExportClick = () =>
|
||||
{
|
||||
if(props.widgetData?.csvData)
|
||||
{
|
||||
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
else if(csv)
|
||||
{
|
||||
const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv";
|
||||
HtmlUtils.download(fileName, csv);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert("There is no data available to export.")
|
||||
alert("There is no data available to export.");
|
||||
}
|
||||
}
|
||||
|
||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||
if(props.widgetMetaData?.showExportButton)
|
||||
{
|
||||
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Widget
|
||||
@ -140,16 +116,13 @@ function TableWidget(props: Props): JSX.Element
|
||||
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
|
||||
footerHTML={props.widgetData?.footerHTML}
|
||||
isChild={props.isChild}
|
||||
labelAdditionalElementsLeft={labelAdditionalElementsLeft}
|
||||
labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []}
|
||||
>
|
||||
<TableCard
|
||||
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
|
||||
rowsPerPage={props.widgetData?.rowsPerPage}
|
||||
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
|
||||
fixedStickyLastRow={props.widgetData?.fixedStickyLastRow}
|
||||
fixedHeight={props.widgetData?.fixedHeight}
|
||||
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
|
||||
widgetMetaData={props.widgetMetaData}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
|
@ -22,7 +22,6 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
|
||||
// Declaring prop types for DataTableBodyCell
|
||||
interface Props
|
||||
@ -39,28 +38,16 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
component="td"
|
||||
textAlign={align}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
px={3}
|
||||
sx={({palette: {light}, typography: {size}, borders: {borderWidth}}: Theme) => ({
|
||||
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
fontSize: "0.875rem",
|
||||
"@media (min-width: 1440px)": {
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"@media (max-width: 1440px)": {
|
||||
fontSize: "0.875rem"
|
||||
},
|
||||
"&:nth-child(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
}
|
||||
fontSize: size.sm,
|
||||
borderBottom: noBorder ? "none" : `${borderWidth[1]} solid ${light.main}`,
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
display="initial"
|
||||
width="max-content"
|
||||
color={colors.dark.main}
|
||||
color="text"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
|
@ -23,7 +23,6 @@ import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {useMaterialUIController} from "qqq/context";
|
||||
|
||||
// Declaring props types for DataTableHeadCell
|
||||
@ -45,30 +44,20 @@ function DataTableHeadCell({width, children, sorted, align, ...rest}: Props): JS
|
||||
component="th"
|
||||
width={width}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
px={3}
|
||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
"&:nth-child(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
},
|
||||
borderBottom: `${borderWidth[1]} solid ${light.main}`,
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
{...rest}
|
||||
sx={({typography: {size, fontWeightBold}}: Theme) => ({
|
||||
position: "relative",
|
||||
color: colors.grey[700],
|
||||
opacity: "0.7",
|
||||
textAlign: align,
|
||||
"@media (min-width: 1440px)": {
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"@media (max-width: 1440px)": {
|
||||
fontSize: "0.875rem"
|
||||
},
|
||||
fontWeight: 600,
|
||||
fontSize: size.xxs,
|
||||
fontWeight: fontWeightBold,
|
||||
textTransform: "uppercase",
|
||||
cursor: sorted && "pointer",
|
||||
userSelect: sorted && "none",
|
||||
})}
|
||||
|
@ -14,31 +14,12 @@ Coded by www.creative-tim.com
|
||||
*/
|
||||
|
||||
import {ReactNode} from "react";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
|
||||
interface Props
|
||||
{
|
||||
isFooter: boolean
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
||||
function DefaultCell({isFooter, children}: Props): JSX.Element
|
||||
function DefaultCell({children}: { children: ReactNode }): JSX.Element
|
||||
{
|
||||
return (
|
||||
<MDTypography variant="button" color={colors.dark.main} sx={{
|
||||
fontWeight: isFooter ? 600 : 500,
|
||||
"@media (min-width: 1440px)": {
|
||||
fontSize: "1rem"
|
||||
},
|
||||
"@media (max-width: 1440px)": {
|
||||
fontSize: "0.875rem"
|
||||
},
|
||||
"& a": {
|
||||
color: colors.blueGray.main
|
||||
}
|
||||
}}>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
{children}
|
||||
</MDTypography>
|
||||
);
|
||||
|
@ -116,7 +116,7 @@ function MaterialUIControllerProvider({children}: { children: ReactNode }): JSX.
|
||||
whiteSidenav: false,
|
||||
sidenavColor: "info",
|
||||
transparentNavbar: true,
|
||||
fixedNavbar: false,
|
||||
fixedNavbar: true,
|
||||
openConfigurator: false,
|
||||
direction: "ltr",
|
||||
layout: "dashboard",
|
||||
|
@ -38,11 +38,11 @@ function DashboardLayout({children}: { children: ReactNode }): JSX.Element
|
||||
return (
|
||||
<Box
|
||||
sx={({breakpoints, transitions, functions: {pxToRem}}) => ({
|
||||
p: "20px",
|
||||
p: 3,
|
||||
position: "relative",
|
||||
|
||||
[breakpoints.up("xl")]: {
|
||||
marginLeft: miniSidenav ? pxToRem(120) : pxToRem(245),
|
||||
marginLeft: miniSidenav ? pxToRem(120) : pxToRem(274),
|
||||
transition: transitions.create(["margin-left", "margin-right"], {
|
||||
easing: transitions.easing.easeInOut,
|
||||
duration: transitions.duration.standard,
|
||||
|
@ -41,45 +41,25 @@
|
||||
** {myLoadingState.isNotLoading() && myData && <Box>...
|
||||
** - In your template, before your "slow loading" view, check for `myLoadingState.isLoadingSlow()`, e.g.
|
||||
** {myLoadingState.isLoadingSlow() && <Spinner />}
|
||||
**
|
||||
** In addition, you can also supply a callback to run "upon slow" (e.g., when
|
||||
** moving into the slow state).
|
||||
*******************************************************************************/
|
||||
export class LoadingState
|
||||
{
|
||||
private state: "notLoading" | "loading" | "slow"
|
||||
private slowTimeout: any;
|
||||
private forceUpdate: () => void;
|
||||
private uponSlowCallback: () => void;
|
||||
private forceUpdate: () => void
|
||||
|
||||
constructor(forceUpdate: () => void, initialState: "notLoading" | "loading" | "slow" = "notLoading")
|
||||
{
|
||||
this.forceUpdate = forceUpdate;
|
||||
this.state = initialState;
|
||||
|
||||
if(initialState == "loading")
|
||||
{
|
||||
this.setLoading();
|
||||
}
|
||||
else if(initialState == "notLoading")
|
||||
{
|
||||
this.setNotLoading();
|
||||
}
|
||||
}
|
||||
|
||||
public setLoading()
|
||||
{
|
||||
clearTimeout(this.slowTimeout);
|
||||
this.state = "loading";
|
||||
this.slowTimeout = setTimeout(() =>
|
||||
{
|
||||
this.state = "slow";
|
||||
|
||||
if(this.uponSlowCallback)
|
||||
{
|
||||
this.uponSlowCallback();
|
||||
}
|
||||
|
||||
this.forceUpdate();
|
||||
}, 1000);
|
||||
}
|
||||
@ -105,14 +85,4 @@ export class LoadingState
|
||||
return (this.state == "notLoading");
|
||||
}
|
||||
|
||||
public getState(): string
|
||||
{
|
||||
return (this.state);
|
||||
}
|
||||
|
||||
public setUponSlowCallback(value: any)
|
||||
{
|
||||
this.uponSlowCallback = value;
|
||||
}
|
||||
|
||||
}
|
@ -1,406 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {GridPinnedColumns} from "@mui/x-data-grid-pro";
|
||||
import quickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
||||
import DataGridUtils from "qqq/utils/DataGridUtils";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
|
||||
/*******************************************************************************
|
||||
** member object
|
||||
*******************************************************************************/
|
||||
interface Column
|
||||
{
|
||||
name: string;
|
||||
isVisible: boolean;
|
||||
width: number;
|
||||
pinned?: "left" | "right";
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Model for all info we'll store about columns on a query screen.
|
||||
*******************************************************************************/
|
||||
export default class QQueryColumns
|
||||
{
|
||||
columns: Column[] = [];
|
||||
|
||||
/*******************************************************************************
|
||||
** factory function - build a QQueryColumns object from JSON (string or parsed object).
|
||||
**
|
||||
** input json is must look like if you JSON.stringify this class - that is:
|
||||
** {columns: [{name:"",isVisible:true,width:0,pinned:"left"},{}...]}
|
||||
*******************************************************************************/
|
||||
public static buildFromJSON = (json: string | any): QQueryColumns =>
|
||||
{
|
||||
const queryColumns = new QQueryColumns();
|
||||
|
||||
if (typeof json == "string")
|
||||
{
|
||||
json = JSON.parse(json);
|
||||
}
|
||||
|
||||
queryColumns.columns = json.columns;
|
||||
|
||||
return (queryColumns);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** factory function - build a default QQueryColumns object for a table
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static buildDefaultForTable = (table: QTableMetaData): QQueryColumns =>
|
||||
{
|
||||
const queryColumns = new QQueryColumns();
|
||||
|
||||
queryColumns.columns = [];
|
||||
queryColumns.columns.push({name: "__check__", isVisible: true, width: 100, pinned: "left"});
|
||||
|
||||
const fields = this.getSortedFieldsFromTable(table);
|
||||
fields.forEach((field) =>
|
||||
{
|
||||
const column: Column = {name: field.name, isVisible: true, width: DataGridUtils.getColumnWidthForField(field, table)};
|
||||
queryColumns.columns.push(column);
|
||||
|
||||
if (field.name == table.primaryKeyField)
|
||||
{
|
||||
column.pinned = "left";
|
||||
}
|
||||
});
|
||||
|
||||
table.exposedJoins?.forEach((exposedJoin) =>
|
||||
{
|
||||
const joinFields = this.getSortedFieldsFromTable(exposedJoin.joinTable);
|
||||
joinFields.forEach((field) =>
|
||||
{
|
||||
const column: Column = {name: `${exposedJoin.joinTable.name}.${field.name}`, isVisible: false, width: DataGridUtils.getColumnWidthForField(field, null)};
|
||||
queryColumns.columns.push(column);
|
||||
});
|
||||
});
|
||||
|
||||
return (queryColumns);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public addColumnForNewField = (table: QTableMetaData, fieldName: string, defaultVisibilityIfInMainTable: boolean): void =>
|
||||
{
|
||||
const [field, tableForField] = TableUtils.getFieldAndTable(table, fieldName)
|
||||
|
||||
let column: Column;
|
||||
if(tableForField.name == table.name)
|
||||
{
|
||||
column = {name: field.name, isVisible: defaultVisibilityIfInMainTable, width: DataGridUtils.getColumnWidthForField(field, table)};
|
||||
}
|
||||
else
|
||||
{
|
||||
column = {name: `${tableForField.name}.${field.name}`, isVisible: false, width: DataGridUtils.getColumnWidthForField(field, null)};
|
||||
}
|
||||
|
||||
this.columns.push(column);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public deleteColumnForOldField = (table: QTableMetaData, fieldName: string): void =>
|
||||
{
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
if(this.columns[i].name == fieldName)
|
||||
{
|
||||
this.columns.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Couldn't find column to be deleted, for name [${fieldName}]`);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static getSortedFieldsFromTable(table: QTableMetaData)
|
||||
{
|
||||
const fields = [...table.fields.values()];
|
||||
fields.sort((a: QFieldMetaData, b: QFieldMetaData) =>
|
||||
{
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public getVisibleColumnCount(): number
|
||||
{
|
||||
let rs = 0;
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
if(this.columns[i].name == "__check__")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(this.columns[i].isVisible)
|
||||
{
|
||||
rs++;
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public getVisibilityToggleStates(): { [name: string]: boolean }
|
||||
{
|
||||
const rs: {[name: string]: boolean} = {};
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
rs[this.columns[i].name] = this.columns[i].isVisible;
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public setIsVisible(name: string, isVisible: boolean)
|
||||
{
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
if(this.columns[i].name == name)
|
||||
{
|
||||
this.columns[i].isVisible = isVisible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public updateVisibility = (visibilityModel: { [name: string]: boolean }): void =>
|
||||
{
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const name = this.columns[i].name;
|
||||
this.columns[i].isVisible = visibilityModel[name];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public updateColumnOrder = (names: string[]): void =>
|
||||
{
|
||||
const newColumns: Column[] = [];
|
||||
const rest: Column[] = [];
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
const index = names.indexOf(column.name);
|
||||
if (index > -1)
|
||||
{
|
||||
newColumns[index] = column;
|
||||
}
|
||||
else
|
||||
{
|
||||
rest.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
this.columns = [...newColumns, ...rest];
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public updateColumnWidth = (name: string, width: number): void =>
|
||||
{
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
if (this.columns[i].name == name)
|
||||
{
|
||||
this.columns[i].width = width;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public setPinnedLeftColumns = (names: string[]): void =>
|
||||
{
|
||||
const leftPins: Column[] = [];
|
||||
const rest: Column[] = [];
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
const pinIndex = names ? names.indexOf(column.name) : -1;
|
||||
if (pinIndex > -1)
|
||||
{
|
||||
column.pinned = "left";
|
||||
leftPins[pinIndex] = column;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (column.pinned == "left")
|
||||
{
|
||||
column.pinned = undefined;
|
||||
}
|
||||
rest.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
this.columns = [...leftPins, ...rest];
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public setPinnedRightColumns = (names: string[]): void =>
|
||||
{
|
||||
const rightPins: Column[] = [];
|
||||
const rest: Column[] = [];
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
const pinIndex = names ? names.indexOf(column.name) : -1;
|
||||
if (pinIndex > -1)
|
||||
{
|
||||
column.pinned = "right";
|
||||
rightPins[pinIndex] = column;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (column.pinned == "right")
|
||||
{
|
||||
column.pinned = undefined;
|
||||
}
|
||||
rest.push(column);
|
||||
}
|
||||
}
|
||||
|
||||
this.columns = [...rest, ...rightPins];
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public getColumnSortValues = (): { [name: string]: number } =>
|
||||
{
|
||||
const sortValues: { [name: string]: number } = {};
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
sortValues[this.columns[i].name] = i;
|
||||
}
|
||||
return sortValues;
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public getColumnWidths = (): { [name: string]: number } =>
|
||||
{
|
||||
const widths: { [name: string]: number } = {};
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
widths[column.name] = column.width;
|
||||
}
|
||||
return widths;
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public toGridPinnedColumns = (): GridPinnedColumns =>
|
||||
{
|
||||
const gridPinnedColumns: GridPinnedColumns = {left: [], right: []};
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
if (column.pinned == "left")
|
||||
{
|
||||
gridPinnedColumns.left.push(column.name);
|
||||
}
|
||||
else if (column.pinned == "right")
|
||||
{
|
||||
gridPinnedColumns.right.push(column.name);
|
||||
}
|
||||
}
|
||||
|
||||
return gridPinnedColumns;
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public toColumnVisibilityModel = (): { [index: string]: boolean } =>
|
||||
{
|
||||
const columnVisibilityModel: { [index: string]: boolean } = {};
|
||||
|
||||
for (let i = 0; i < this.columns.length; i++)
|
||||
{
|
||||
const column = this.columns[i];
|
||||
columnVisibilityModel[column.name] = column.isVisible;
|
||||
}
|
||||
|
||||
return columnVisibilityModel;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** subclass of QQueryColumns - used as a marker, to indicate that the table
|
||||
** isn't yet loaded, so it just a placeholder.
|
||||
*******************************************************************************/
|
||||
export class PreLoadQueryColumns extends QQueryColumns
|
||||
{
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* 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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import QQueryColumns, {PreLoadQueryColumns} from "qqq/models/query/QQueryColumns";
|
||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Model to represent the full "view" that is active on the RecordQuery screen
|
||||
** (and accordingly, can be saved as a saved view).
|
||||
*******************************************************************************/
|
||||
export default class RecordQueryView
|
||||
{
|
||||
queryFilter: QQueryFilter; // contains orderBys
|
||||
queryColumns: QQueryColumns; // contains on/off, sequence, widths, and pins
|
||||
viewIdentity: string; // url vs. saved vs. ad-hoc, plus "noncey" stuff? not very used...
|
||||
rowsPerPage: number;
|
||||
quickFilterFieldNames: string[];
|
||||
mode: string;
|
||||
// variant?
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
constructor()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** factory function - build a RecordQueryView object from JSON (string or parsed object).
|
||||
**
|
||||
** input json is must look like if you JSON.stringify this class - that is:
|
||||
** {queryFilter: {}, queryColumns: {}, etc...}
|
||||
*******************************************************************************/
|
||||
public static buildFromJSON = (json: string | any): RecordQueryView =>
|
||||
{
|
||||
const view = new RecordQueryView();
|
||||
|
||||
if (typeof json == "string")
|
||||
{
|
||||
json = JSON.parse(json);
|
||||
}
|
||||
|
||||
view.queryFilter = json.queryFilter as QQueryFilter;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// it's important that some criteria values exist as expression objects - so - do that. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
for (let i = 0; i < view.queryFilter?.criteria?.length; i++)
|
||||
{
|
||||
const criteria = view.queryFilter.criteria[i]
|
||||
for (let j = 0; j < criteria?.values?.length; j++)
|
||||
{
|
||||
const value = criteria.values[j];
|
||||
const expression = FilterUtils.gridCriteriaValueToExpression(value);
|
||||
if(expression)
|
||||
{
|
||||
criteria.values[j] = expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(json.queryColumns)
|
||||
{
|
||||
view.queryColumns = QQueryColumns.buildFromJSON(json.queryColumns);
|
||||
}
|
||||
else
|
||||
{
|
||||
view.queryColumns = new PreLoadQueryColumns();
|
||||
}
|
||||
|
||||
view.viewIdentity = json.viewIdentity;
|
||||
view.rowsPerPage = json.rowsPerPage;
|
||||
view.quickFilterFieldNames = json.quickFilterFieldNames;
|
||||
view.mode = json.mode;
|
||||
|
||||
return (view);
|
||||
};
|
||||
|
||||
}
|
@ -34,7 +34,6 @@ import Grid from "@mui/material/Grid";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {Link, useLocation} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import ProcessLinkCard from "qqq/components/processes/ProcessLinkCard";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
@ -75,17 +74,9 @@ function AppHome({app}: Props): JSX.Element
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const mdbMetaData = app?.supplementalAppMetaData?.get("materialDashboard");
|
||||
let showAppLabelOnHomeScreen = true;
|
||||
if(mdbMetaData)
|
||||
{
|
||||
showAppLabelOnHomeScreen = mdbMetaData.showAppLabelOnHomeScreen;
|
||||
}
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
// setPageHeader(app.label);
|
||||
setPageHeader(null);
|
||||
setPageHeader(app.label);
|
||||
|
||||
if (!qInstance)
|
||||
{
|
||||
@ -189,6 +180,9 @@ function AppHome({app}: Props): JSX.Element
|
||||
}
|
||||
}, [qInstance, location]);
|
||||
|
||||
const widgetCount = widgets ? widgets.length : 0;
|
||||
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const tileSizeLg = 3;
|
||||
|
||||
const hasTablePermission = (tableName: string) =>
|
||||
@ -206,80 +200,13 @@ function AppHome({app}: Props): JSX.Element
|
||||
return reports.find(r => r.name === reportName && r.hasPermission);
|
||||
};
|
||||
|
||||
const widgetCount = widgets ? widgets.length : 0;
|
||||
const sectionCount = app.sections ? app.sections.length : 0;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if our app has no widgets or sections, but it does have child apps, then return those child apps //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(widgetCount == 0 && sectionCount == 0 && childApps && childApps.length > 0)
|
||||
{
|
||||
return (
|
||||
<BaseLayout>
|
||||
{
|
||||
showAppLabelOnHomeScreen &&
|
||||
<Typography textTransform="capitalize" variant="h3">
|
||||
{app.label}
|
||||
</Typography>
|
||||
}
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={12}>
|
||||
<Card sx={{overflow: "visible"}}>
|
||||
<Box p={3} display="flex" alignItems="center" gap=".5rem">
|
||||
<Typography variant="h5">Apps</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={3} padding={3} pt={0}>
|
||||
{childApps.map((childApp) => (
|
||||
<Grid key={childApp.name} item xs={12} lg={3}>
|
||||
<Link to={childApp.name}>
|
||||
<Card>
|
||||
<Box display="flex" alignItems="center" p={2}>
|
||||
<Box
|
||||
color={"#FFFFFF"}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="4rem"
|
||||
height="4rem"
|
||||
sx={{borderRadius: "10px", backgroundColor: colors.info.main}}
|
||||
>
|
||||
<Icon fontSize="medium" color="inherit">
|
||||
{childApp.iconName || app.iconName}
|
||||
</Icon>
|
||||
</Box>
|
||||
<Box textAlign="left" ml={2}>
|
||||
<MDTypography variant="button" fontWeight="bold" color="text">
|
||||
{childApp.label}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
</Link>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
{
|
||||
showAppLabelOnHomeScreen &&
|
||||
<Typography textTransform="capitalize" variant="h3">
|
||||
{app.label}
|
||||
</Typography>
|
||||
}
|
||||
<Box>
|
||||
{app.widgets && app.widgets.length > 0 && (
|
||||
<Box pb={app.sections ? 2.375 : 4} pt={"0.5rem"}>
|
||||
<DashboardWidgets widgetMetaDataList={widgets} />
|
||||
</Box>
|
||||
{app.widgets && (
|
||||
<DashboardWidgets widgetMetaDataList={widgets} />
|
||||
)}
|
||||
<Grid container spacing={3} mt={"-1rem"}>
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
app.sections ? (
|
||||
<Grid item xs={12} lg={12}>
|
||||
|
@ -229,7 +229,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const download = (url: string, fileName: string) =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - this could be simplified, i think? //
|
||||
// todo - this could be simplified. //
|
||||
// it was originally built like this when we had to submit full access token to backend... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let xhr = new XMLHttpRequest();
|
||||
@ -237,6 +237,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
xhr.responseType = "blob";
|
||||
let formData = new FormData();
|
||||
|
||||
////////////////////////////////////
|
||||
// todo#authHeader - delete this. //
|
||||
////////////////////////////////////
|
||||
const qController = Client.getInstance();
|
||||
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||
|
||||
// @ts-ignore
|
||||
xhr.send(formData);
|
||||
|
||||
@ -323,7 +329,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
<Grid m={3} mt={9} container>
|
||||
<Grid item xs={0} lg={3} />
|
||||
<Grid item xs={12} lg={6}>
|
||||
<Card>
|
||||
<Card elevation={5}>
|
||||
<Box p={3}>
|
||||
<MDTypography variant="h5" component="div">
|
||||
Working
|
||||
@ -414,238 +420,228 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
//////////////////////////////////////////////////
|
||||
// render all of the components for this screen //
|
||||
//////////////////////////////////////////////////
|
||||
step.components && (step.components.map((component: QFrontendComponent, index: number) =>
|
||||
{
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]
|
||||
if(component.type == QComponentType.BULK_EDIT_FORM)
|
||||
{
|
||||
helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{
|
||||
component.type === QComponentType.HELP_TEXT && (
|
||||
component.values.previewText ?
|
||||
<>
|
||||
<Box mt={1}>
|
||||
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
|
||||
{showFullHelpText ? "Hide " : "Show "}
|
||||
{component.values.previewText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
|
||||
<Typography variant="body2" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
:
|
||||
<MDTypography variant="button" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</MDTypography>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.BULK_EDIT_FORM && (
|
||||
tableMetaData && localTableSections ?
|
||||
<Grid container spacing={3} mt={2}>
|
||||
step.components && (step.components.map((component: QFrontendComponent, index: number) => (
|
||||
<div key={index}>
|
||||
{
|
||||
component.type === QComponentType.HELP_TEXT && (
|
||||
component.values.previewText ?
|
||||
<>
|
||||
<Box mt={1}>
|
||||
<Button onClick={toggleShowFullHelpText} startIcon={<Icon>{showFullHelpText ? "expand_less" : "expand_more"}</Icon>} sx={{pl: 1}}>
|
||||
{showFullHelpText ? "Hide " : "Show "}
|
||||
{component.values.previewText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box mt={1} style={{display: showFullHelpText ? "block" : "none"}}>
|
||||
<Typography variant="body2" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
:
|
||||
<MDTypography variant="button" color="info">
|
||||
{ValueUtils.breakTextIntoLines(component.values.text)}
|
||||
</MDTypography>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.BULK_EDIT_FORM && (
|
||||
tableMetaData && localTableSections ?
|
||||
<Grid container spacing={3} mt={2}>
|
||||
{
|
||||
localTableSections.length == 0 &&
|
||||
<Grid item xs={12}>
|
||||
<Alert color="error">There are no editable fields on this table.</Alert>
|
||||
</Grid>
|
||||
}
|
||||
<Grid item xs={12} lg={3}>
|
||||
{
|
||||
localTableSections.length == 0 &&
|
||||
<Grid item xs={12}>
|
||||
<Alert color="error">There are no editable fields on this table.</Alert>
|
||||
</Grid>
|
||||
localTableSections.length > 0 && <QRecordSidebar tableSections={localTableSections} stickyTop="20px" />
|
||||
}
|
||||
<Grid item xs={12} lg={3}>
|
||||
{
|
||||
localTableSections.length > 0 && <QRecordSidebar tableSections={localTableSections} stickyTop="20px" />
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={9}>
|
||||
{
|
||||
localTableSections.map((section: QTableSection, index: number) =>
|
||||
{
|
||||
const name = section.name;
|
||||
|
||||
if (section.isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionFormFields = {};
|
||||
for (let i = 0; i < section.fieldNames.length; i++)
|
||||
{
|
||||
const fieldName = section.fieldNames[i];
|
||||
if (formFields[fieldName])
|
||||
{
|
||||
// @ts-ignore
|
||||
sectionFormFields[fieldName] = formFields[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(sectionFormFields).length > 0)
|
||||
{
|
||||
const sectionFormData = {
|
||||
formFields: sectionFormFields,
|
||||
values: values,
|
||||
errors: errors,
|
||||
touched: touched
|
||||
};
|
||||
|
||||
return (
|
||||
<Box key={name} pb={3}>
|
||||
<Card id={name} sx={{scrollMarginTop: "20px"}} elevation={5}>
|
||||
<MDTypography variant="h5" p={3} pb={1}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
<Box px={2}>
|
||||
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<br />);
|
||||
}
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} helpRoles={helpRoles} helpContentKeyPrefix={`table:${tableMetaData?.name};`} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.EDIT_FORM && (
|
||||
<QDynamicForm formData={formData} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||
<div>
|
||||
{step.viewFields.map((field: QFieldMetaData) => (
|
||||
field.hasAdornment(AdornmentType.ERROR) ? (
|
||||
processValues[field.name] && (
|
||||
<Box key={field.name} display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="regular">
|
||||
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Grid item xs={12} lg={9}>
|
||||
|
||||
{localTableSections.map((section: QTableSection, index: number) =>
|
||||
{
|
||||
const name = section.name;
|
||||
|
||||
if (section.isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionFormFields = {};
|
||||
for (let i = 0; i < section.fieldNames.length; i++)
|
||||
{
|
||||
const fieldName = section.fieldNames[i];
|
||||
if (formFields[fieldName])
|
||||
{
|
||||
// @ts-ignore
|
||||
sectionFormFields[fieldName] = formFields[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(sectionFormFields).length > 0)
|
||||
{
|
||||
const sectionFormData = {
|
||||
formFields: sectionFormFields,
|
||||
values: values,
|
||||
errors: errors,
|
||||
touched: touched
|
||||
};
|
||||
|
||||
return (
|
||||
<Box key={name} pb={3}>
|
||||
<Card id={name} sx={{scrollMarginTop: "20px"}} elevation={5}>
|
||||
<MDTypography variant="h5" p={3} pb={1}>
|
||||
{section.label}
|
||||
</MDTypography>
|
||||
<Box px={2}>
|
||||
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (<br />);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.EDIT_FORM && (
|
||||
<QDynamicForm formData={formData} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||
<div>
|
||||
{step.viewFields.map((field: QFieldMetaData) => (
|
||||
field.hasAdornment(AdornmentType.ERROR) ? (
|
||||
processValues[field.name] && (
|
||||
<Box key={field.name} display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="bold">
|
||||
{field.label}
|
||||
:
|
||||
</MDTypography>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
<MDTypography variant="button" fontWeight="regular">
|
||||
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
)))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.DOWNLOAD_FORM && (
|
||||
<Grid container display="flex" justifyContent="center">
|
||||
<Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
|
||||
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
|
||||
Download
|
||||
</Box>
|
||||
<Box display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="bold" onClick={() => download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}>
|
||||
<Box display="flex" alignItems="center" gap={1} py={1} pr={2}>
|
||||
<Icon fontSize="large">download_for_offline</Icon>
|
||||
{processValues.downloadFileName}
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<Box key={field.name} display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="bold">
|
||||
{field.label}
|
||||
:
|
||||
</MDTypography>
|
||||
<MDTypography variant="button" fontWeight="regular" color="text">
|
||||
{ValueUtils.getValueForDisplay(field, processValues[field.name], undefined, "view")}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
|
||||
<ValidationReview
|
||||
qInstance={qInstance}
|
||||
process={processMetaData}
|
||||
table={tableMetaData}
|
||||
processValues={processValues}
|
||||
step={step}
|
||||
previewRecords={records}
|
||||
formValues={formData.values}
|
||||
doFullValidationRadioChangedHandler={(event: any) =>
|
||||
{
|
||||
const {value} = event.currentTarget;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// call the formik function to set the value in this field. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
setFieldValue("doFullValidation", value);
|
||||
|
||||
setOverrideOnLastStep(value !== "true");
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
|
||||
<ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
|
||||
// todo - make these booleans configurable (values on the component)
|
||||
<GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
|
||||
<div>
|
||||
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
|
||||
{" "}
|
||||
<br />
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
components={{Pagination: CustomPagination}}
|
||||
page={recordConfig.pageNo}
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
rows={recordConfig.rows}
|
||||
columns={recordConfig.columns}
|
||||
rowBuffer={10}
|
||||
rowCount={recordConfig.totalRecords}
|
||||
pageSize={recordConfig.rowsPerPage}
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
onPageSizeChange={recordConfig.handleRowsPerPageChange}
|
||||
onPageChange={recordConfig.handlePageChange}
|
||||
onRowClick={recordConfig.handleRowClick}
|
||||
getRowId={(row) => row.__idForDataGridPro__}
|
||||
paginationMode="server"
|
||||
pagination
|
||||
density="compact"
|
||||
loading={recordConfig.loading}
|
||||
disableColumnFilter
|
||||
/>
|
||||
)))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.DOWNLOAD_FORM && (
|
||||
<Grid container display="flex" justifyContent="center">
|
||||
<Grid item xs={12} sm={12} xl={8} m={3} p={3} mt={6} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
|
||||
<Box mx={2} mt={-6} p={1} sx={{width: "fit-content", borderColor: "rgb(70%, 70%, 70%)", borderWidth: "2px", borderStyle: "solid", borderRadius: ".25em", backgroundColor: "#FFFFFF"}} width="initial" color="white">
|
||||
Download
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.HTML && (
|
||||
processValues[`${step.name}.html`] &&
|
||||
<Box fontSize="1rem">
|
||||
{parse(processValues[`${step.name}.html`])}
|
||||
<Box display="flex" py={1} pr={2}>
|
||||
<MDTypography variant="button" fontWeight="bold" onClick={() => download(`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`, processValues.downloadFileName)} sx={{cursor: "pointer"}}>
|
||||
<Box display="flex" alignItems="center" gap={1} py={1} pr={2}>
|
||||
<Icon fontSize="large">download_for_offline</Icon>
|
||||
{processValues.downloadFileName}
|
||||
</Box>
|
||||
</MDTypography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
|
||||
<ValidationReview
|
||||
qInstance={qInstance}
|
||||
process={processMetaData}
|
||||
table={tableMetaData}
|
||||
processValues={processValues}
|
||||
step={step}
|
||||
previewRecords={records}
|
||||
formValues={formData.values}
|
||||
doFullValidationRadioChangedHandler={(event: any) =>
|
||||
{
|
||||
const {value} = event.currentTarget;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// call the formik function to set the value in this field. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
setFieldValue("doFullValidation", value);
|
||||
|
||||
setOverrideOnLastStep(value !== "true");
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.PROCESS_SUMMARY_RESULTS && (
|
||||
<ProcessSummaryResults qInstance={qInstance} process={processMetaData} table={tableMetaData} processValues={processValues} step={step} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.GOOGLE_DRIVE_SELECT_FOLDER && (
|
||||
// todo - make these booleans configurable (values on the component)
|
||||
<GoogleDriveFolderPickerWrapper showSharedDrivesView={true} showDefaultFoldersView={false} qInstance={qInstance} />
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.RECORD_LIST && step.recordListFields && recordConfig.columns && (
|
||||
<div>
|
||||
<MDTypography variant="button" fontWeight="bold">Records</MDTypography>
|
||||
{" "}
|
||||
<br />
|
||||
<Box height="100%">
|
||||
<DataGridPro
|
||||
components={{Pagination: CustomPagination}}
|
||||
page={recordConfig.pageNo}
|
||||
disableSelectionOnClick
|
||||
autoHeight
|
||||
rows={recordConfig.rows}
|
||||
columns={recordConfig.columns}
|
||||
rowBuffer={10}
|
||||
rowCount={recordConfig.totalRecords}
|
||||
pageSize={recordConfig.rowsPerPage}
|
||||
rowsPerPageOptions={[10, 25, 50]}
|
||||
onPageSizeChange={recordConfig.handleRowsPerPageChange}
|
||||
onPageChange={recordConfig.handlePageChange}
|
||||
onRowClick={recordConfig.handleRowClick}
|
||||
getRowId={(row) => row.__idForDataGridPro__}
|
||||
paginationMode="server"
|
||||
pagination
|
||||
density="compact"
|
||||
loading={recordConfig.loading}
|
||||
disableColumnFilter
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
component.type === QComponentType.HTML && (
|
||||
processValues[`${step.name}.html`] &&
|
||||
<Box fontSize="1rem">
|
||||
{parse(processValues[`${step.name}.html`])}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1113,7 +1109,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
if (recordIds instanceof QQueryFilter)
|
||||
{
|
||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||
queryStringPairsForInit.push(`filterJSON=${encodeURIComponent(JSON.stringify(recordIds))}`);
|
||||
queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`);
|
||||
}
|
||||
else if (typeof recordIds === "object" && recordIds.length)
|
||||
{
|
||||
@ -1126,7 +1122,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
queryStringPairsForInit.push(`tableVariant=${encodeURIComponent(JSON.stringify(tableVariant))}`);
|
||||
queryStringPairsForInit.push(`tableVariant=${JSON.stringify(tableVariant)}`);
|
||||
}
|
||||
|
||||
try
|
||||
@ -1281,14 +1277,13 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
||||
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
||||
{
|
||||
mainCardStyles.background = "#FFFFFF";
|
||||
mainCardStyles.background = "none";
|
||||
mainCardStyles.boxShadow = "none";
|
||||
}
|
||||
if (isWidget)
|
||||
{
|
||||
mainCardStyles.background = "none";
|
||||
mainCardStyles.boxShadow = "none";
|
||||
mainCardStyles.border = "none";
|
||||
mainCardStyles.minHeight = "";
|
||||
mainCardStyles.alignItems = "stretch";
|
||||
mainCardStyles.flexGrow = 1;
|
||||
|
@ -34,8 +34,12 @@ function EntityCreate({table}: Props): JSX.Element
|
||||
{
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Box mb={3}>
|
||||
<EntityForm table={table} />
|
||||
<Box mt={4}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={12}>
|
||||
<EntityForm table={table} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</BaseLayout>
|
||||
);
|
||||
|
@ -43,8 +43,18 @@ function EntityEdit({table, isCopy}: Props): JSX.Element
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<Box mb={3}>
|
||||
<EntityForm table={table} id={id} isCopy={isCopy} />
|
||||
<Box mt={4}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={12}>
|
||||
<Box mb={3}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<EntityForm table={table} id={id} isCopy={isCopy} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</BaseLayout>
|
||||
);
|
||||
|
@ -21,7 +21,6 @@
|
||||
|
||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||
@ -147,11 +146,6 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
||||
const rows = DataGridUtils.makeRows(valueCounts, fakeTableMetaData);
|
||||
const columns = DataGridUtils.setupGridColumns(fakeTableMetaData, null, null, "bySection");
|
||||
|
||||
if(fieldMetaData.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
columns[0].headerName = fieldMetaData.label + " (grouped by hour)"
|
||||
}
|
||||
|
||||
columns.forEach((c) =>
|
||||
{
|
||||
c.filterable = false;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -190,7 +190,7 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
||||
return (
|
||||
<div key={fieldName}>
|
||||
<Card sx={{mb: 3}}>
|
||||
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||
<Typography variant="h6" p={2} pl={3} pb={1}>{field?.label}</Typography>
|
||||
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
||||
{scriptId ?
|
||||
|
@ -45,16 +45,13 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import AuditBody from "qqq/components/audits/AuditBody";
|
||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
@ -101,7 +98,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const [metaData, setMetaData] = useState(null as QInstance);
|
||||
const [record, setRecord] = useState(null as QRecord);
|
||||
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
||||
const [t1Section, setT1Section] = useState(null as QTableSection);
|
||||
const [t1SectionName, setT1SectionName] = useState(null as string);
|
||||
const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element);
|
||||
const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]);
|
||||
@ -121,7 +117,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||
const closeActionsMenu = () => setActionsMenu(null);
|
||||
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive} = useContext(QContext);
|
||||
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
|
||||
|
||||
if (localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
@ -355,23 +351,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
return (visibleJoinTables);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get an element (or empty) to use as help content for a section
|
||||
*******************************************************************************/
|
||||
const getSectionHelp = (section: QTableSection) =>
|
||||
{
|
||||
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
|
||||
const formattedHelpContent = <HelpContent helpContents={section.helpContents} roles={helpRoles} helpContentKey={`table:${tableName};section:${section.name}`} />;
|
||||
|
||||
return formattedHelpContent && (
|
||||
<Box px={"1.5rem"} fontSize={"0.875rem"} color={colors.blueGray.main}>
|
||||
{formattedHelpContent}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (!asyncLoadInited)
|
||||
{
|
||||
setAsyncLoadInited(true);
|
||||
@ -462,17 +441,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
setPageHeader(record.recordLabel);
|
||||
|
||||
if(!launchingProcess)
|
||||
if(record)
|
||||
{
|
||||
try
|
||||
setPageHeader(record.recordLabel);
|
||||
|
||||
if(!launchingProcess)
|
||||
{
|
||||
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error("Error pushing history: " + e);
|
||||
try
|
||||
{
|
||||
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.error("Error pushing history: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -523,24 +505,15 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||
let label = field.label;
|
||||
|
||||
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
|
||||
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
|
||||
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={label} helpContentKey={`table:${tableName};field:${fieldName}`} />;
|
||||
|
||||
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default"}}>{label}:</Typography>
|
||||
|
||||
return (
|
||||
<Box key={fieldName} flexDirection="row" pr={2}>
|
||||
<>
|
||||
{
|
||||
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
|
||||
}
|
||||
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)">
|
||||
{label}:
|
||||
<div style={{display: "inline-block", width: 0}}> </div>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
</Typography>
|
||||
</>
|
||||
</Typography>
|
||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
|
||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@ -561,7 +534,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
<Typography variant="h6" p={3} pb={1}>
|
||||
{section.label}
|
||||
</Typography>
|
||||
{getSectionHelp(section)}
|
||||
<Box p={3} pt={0} flexDirection="column">
|
||||
{fields}
|
||||
</Box>
|
||||
@ -580,7 +552,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{
|
||||
setT1SectionElement(sectionFieldElements.get(section.name));
|
||||
setT1SectionName(section.name);
|
||||
setT1Section(section);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -911,7 +882,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
||||
{renderActionsMenu}
|
||||
</Box>
|
||||
</Box>
|
||||
{t1Section && getSectionHelp(t1Section)}
|
||||
{t1SectionElement ? (<Box p={3} pt={0}>{t1SectionElement}</Box>) : null}
|
||||
</Card>
|
||||
</Grid>
|
||||
|
@ -29,13 +29,6 @@
|
||||
min-height: calc(100vh - 450px) !important;
|
||||
}
|
||||
|
||||
/* we want to leave columns w/ the sortable attribute (so they have it in the column menu),
|
||||
but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
.recordQuery .MuiDataGrid-columnHeader--sortable
|
||||
{
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Disable red outlines on clicked cells */
|
||||
.MuiDataGrid-cell:focus,
|
||||
.MuiDataGrid-columnHeader:focus,
|
||||
@ -107,12 +100,9 @@ but we've turned off the click-to-sort function, so remove hand cursor */
|
||||
}
|
||||
|
||||
/* move the green check / red x down to align with the calendar icon */
|
||||
.MuiFormControl-root:has(input[type="datetime-local"]),
|
||||
.MuiFormControl-root:has(input[type="date"]),
|
||||
.MuiFormControl-root:has(input[type="time"]),
|
||||
.MuiFormControl-root:has(.MuiInputBase-inputAdornedEnd)
|
||||
.MuiFormControl-root
|
||||
{
|
||||
background-position: right 2rem center;
|
||||
background-position-y: 1.4rem !important;
|
||||
}
|
||||
|
||||
.MuiInputAdornment-sizeMedium *
|
||||
@ -409,8 +399,7 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.custom-columns-panel .MuiSwitch-thumb,
|
||||
.fieldListMenuBody .MuiSwitch-thumb
|
||||
.custom-columns-panel .MuiSwitch-thumb
|
||||
{
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
@ -432,20 +421,6 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
top: -60px !important;
|
||||
}
|
||||
|
||||
.MuiDataGrid-panel:has(.customFilterPanel)
|
||||
{
|
||||
/* overwrite what the grid tries to do here, where it changes based on density... we always want the same. */
|
||||
/* transform: translate(274px, 305px) !important; */
|
||||
transform: translate(274px, 264px) !important;
|
||||
}
|
||||
|
||||
/* within the data-grid, the filter-panel's container has a max-height. mirror that, and set an overflow-y */
|
||||
.MuiDataGrid-panel .customFilterPanel
|
||||
{
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* tighten the text in the field select dropdown in custom filters */
|
||||
.customFilterPanel .MuiAutocomplete-paper
|
||||
{
|
||||
@ -509,8 +484,7 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
}
|
||||
|
||||
/* change tags in any-of value fields to not be black bg with white text */
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root,
|
||||
.quickFilter.filterValuesColumn .MuiChip-root
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root
|
||||
{
|
||||
background: none;
|
||||
color: black;
|
||||
@ -518,23 +492,20 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
}
|
||||
|
||||
/* change 'x' icon in tags in any-of value */
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon,
|
||||
.quickFilter.filterValuesColumn .MuiChip-root .MuiChip-deleteIcon
|
||||
.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon
|
||||
{
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* change tags in any-of value fields to not be black bg with white text */
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag,
|
||||
.quickFilter.filterValuesColumn .MuiAutocomplete-tag
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag
|
||||
{
|
||||
color: #191919;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* default hover color for the 'x' to remove a tag from an 'any-of' value was white, which made it disappear */
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover,
|
||||
.quickFilter.filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover
|
||||
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover
|
||||
{
|
||||
color: lightgray;
|
||||
}
|
||||
@ -593,68 +564,3 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
||||
display: inline;
|
||||
right: .5rem
|
||||
}
|
||||
|
||||
/* help-content */
|
||||
.helpContent
|
||||
{
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.helpContent .header
|
||||
{
|
||||
color: #212121;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.MuiTooltip-tooltip .helpContent P + P,
|
||||
.MuiTooltip-tooltip .helpContent UL + P
|
||||
{
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.helpContent UL
|
||||
{
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* for query screen column-header tooltips, move them up a little bit, to be more closely attached to the text. */
|
||||
.dataGridHeaderTooltip
|
||||
{
|
||||
top: -1.25rem;
|
||||
}
|
||||
|
||||
/* when grid contents weren't filling the height of the screen, the gray panel for pinned columns
|
||||
was stretching to most of the grid height, but it wasn't the full height and so looked a little
|
||||
broken. just turing off this min height changes to not try to stretch at all, and is not broken. */
|
||||
.MuiDataGrid-pinnedColumns
|
||||
{
|
||||
min-height: unset !important;
|
||||
}
|
||||
|
||||
/* new style for toggle buttons */
|
||||
.MuiToggleButtonGroup-root
|
||||
{
|
||||
padding: 0.25rem;
|
||||
border: 1px solid #BDBDBD;
|
||||
border-radius: 0.5rem !important;
|
||||
}
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root
|
||||
{
|
||||
text-transform: none;
|
||||
font-size: 0.75rem;
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
border-radius: 0.375rem !important; /* overriding left/right edge overrides for first/last */
|
||||
border: none;
|
||||
flex: 1 1 0px;
|
||||
}
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root.Mui-selected
|
||||
{
|
||||
background: rgba(117, 117, 117, 0.20);
|
||||
}
|
||||
.MuiToggleButtonGroup-root .MuiButtonBase-root.Mui-disabled
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
|
@ -25,13 +25,10 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
|
||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||
import {GridColDef, GridFilterItem, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
||||
import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro";
|
||||
import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator";
|
||||
import {GridColumnHeaderParams} from "@mui/x-data-grid/models/params/gridColumnHeaderParams";
|
||||
import React from "react";
|
||||
import {Link, NavigateFunction} from "react-router-dom";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import {Link} from "react-router-dom";
|
||||
import {buildQGridPvsOperators, QGridBlobOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
@ -84,36 +81,6 @@ const QGridDateTimeOperators = [
|
||||
|
||||
export default class DataGridUtils
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static handleRowClick = (path: string, event: MuiEvent<React.MouseEvent>, gridMouseDownX: number, gridMouseDownY: number, navigate: NavigateFunction, instance: any) =>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// strategy for when to trigger or not trigger a row click: //
|
||||
// To avoid a drag-event that highlighted text in a cell: //
|
||||
// - we capture the x & y upon mouse-down - then compare them in this method (which fires when the mouse is up) //
|
||||
// if they are more than 5 pixels away from the mouse-down, then assume it's a drag, not a click. //
|
||||
// - avoid clicking the row upon double-click, by setting a 500ms timer here - and in the onDoubleClick handler, //
|
||||
// cancelling the timer. //
|
||||
// - also avoid a click, then click-again-and-start-dragging, by always cancelling the timer in mouse-down. //
|
||||
// All in, these seem to have good results - the only downside being the half-second delay... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY));
|
||||
if (diff < 5)
|
||||
{
|
||||
console.log("clearing timeout");
|
||||
clearTimeout(instance.current.timer);
|
||||
instance.current.timer = setTimeout(() =>
|
||||
{
|
||||
navigate(path);
|
||||
}, 100);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -343,20 +310,6 @@ export default class DataGridUtils
|
||||
(cellValues.value)
|
||||
);
|
||||
|
||||
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
|
||||
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
|
||||
if(showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
|
||||
column.renderHeader = (params: GridColumnHeaderParams) => (
|
||||
<Tooltip title={formattedHelpContent}>
|
||||
<div className="MuiDataGrid-columnHeaderTitle" style={{lineHeight: "initial"}}>
|
||||
{headerName}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (column);
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user