QQQ-30: got rid of 'prettier', first pass at eslint configuration pointing only to qqq specific files, reformated all qqq files

This commit is contained in:
Tim Chamberlain
2022-07-12 11:35:24 -05:00
parent cc324fd76d
commit 87d3f070fe
26 changed files with 2099 additions and 1959 deletions

View File

@ -3,7 +3,20 @@
"browser": true, "browser": true,
"es2021": true "es2021": true
}, },
"extends": ["plugin:react/recommended", "airbnb", "prettier"], "extends": [
"plugin:react/recommended",
"airbnb"
],
"globals": {
"JSX": true
},
"ignorePatterns": [
"src/assets",
"src/components",
"src/context",
"src/examples",
"src/layouts"
],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "ecmaFeatures": {
@ -12,37 +25,68 @@
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["react", "@typescript-eslint", "prettier"], "plugins": [
"react",
"@typescript-eslint"
],
"rules": { "rules": {
"prettier/prettier": [ "brace-style": [
2,
"allman"
],
"indent": [
"error", "error",
3
],
"import/extensions": [
"error",
"ignorePackages",
{ {
"endOfLine": "auto" "ts": "never",
"tsx": "never",
"js": "never"
} }
], ],
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
],
"max-len": "off",
"no-console": "off", "no-console": "off",
"react/prop-types": "off",
"no-shadow": "off", "no-shadow": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-plusplus": "off", "no-plusplus": "off",
"spaced-comment": "off", "spaced-comment": "off",
"object-shorthand": "off", "object-shorthand": "off",
"react/prop-types": "off",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".tsx"
]
}
],
"react/jsx-indent": [
"error",
3
],
"react/jsx-indent-props": [
"error",
3
],
"react/jsx-props-no-spreading": "off", "react/jsx-props-no-spreading": "off",
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
"import/extensions": [ "quotes": [
"error", "error",
"ignorePackages", "double"
{ "ts": "never", "tsx": "never", "js": "never" } ]
],
"react/jsx-filename-extension": ["warn", { "extensions": [".tsx"] }],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
}, },
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"typescript": {} "typescript": {}
} }
},
"globals": {
"JSX": true
} }
} }

View File

@ -1,7 +0,0 @@
{
"printWidth": 100,
"trailingComma": "es5",
"semi": true,
"singleQuote": false,
"endOfLine": "auto"
}

102
package-lock.json generated
View File

@ -63,14 +63,11 @@
"@typescript-eslint/parser": "5.10.2", "@typescript-eslint/parser": "5.10.2",
"eslint": "8.8.0", "eslint": "8.8.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-typescript": "2.5.0", "eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1", "eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.28.0", "eslint-plugin-react": "7.28.0",
"eslint-plugin-react-hooks": "4.3.0", "eslint-plugin-react-hooks": "4.3.0",
"prettier": "2.5.1",
"typescript": "^4.7.3" "typescript": "^4.7.3"
} }
}, },
@ -7533,18 +7530,6 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz",
"integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-config-react-app": { "node_modules/eslint-config-react-app": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
@ -7756,27 +7741,6 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/eslint-plugin-prettier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
"integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==",
"dev": true,
"dependencies": {
"prettier-linter-helpers": "^1.0.0"
},
"engines": {
"node": ">=6.0.0"
},
"peerDependencies": {
"eslint": ">=7.28.0",
"prettier": ">=2.0.0"
},
"peerDependenciesMeta": {
"eslint-config-prettier": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.28.0", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz",
@ -8363,12 +8327,6 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"node_modules/fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@ -13484,30 +13442,6 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"dependencies": {
"fast-diff": "^1.1.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/pretty-bytes": { "node_modules/pretty-bytes": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -22393,12 +22327,6 @@
} }
} }
}, },
"eslint-config-prettier": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz",
"integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==",
"dev": true
},
"eslint-config-react-app": { "eslint-config-react-app": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz",
@ -22561,15 +22489,6 @@
} }
} }
}, },
"eslint-plugin-prettier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
"integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.28.0", "version": "7.28.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz",
@ -22955,12 +22874,6 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-glob": { "fast-glob": {
"version": "3.2.11", "version": "3.2.11",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
@ -26510,21 +26423,6 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
}, },
"prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
"dev": true
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"pretty-bytes": { "pretty-bytes": {
"version": "5.6.0", "version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",

View File

@ -87,14 +87,11 @@
"@typescript-eslint/parser": "5.10.2", "@typescript-eslint/parser": "5.10.2",
"eslint": "8.8.0", "eslint": "8.8.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-typescript": "2.5.0", "eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.5.1", "eslint-plugin-jsx-a11y": "6.5.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.28.0", "eslint-plugin-react": "7.28.0",
"eslint-plugin-react-hooks": "4.3.0", "eslint-plugin-react-hooks": "4.3.0",
"prettier": "2.5.1",
"typescript": "^4.7.3" "typescript": "^4.7.3"
}, },
"main": "qqq-frontend-material-dashboard.js", "main": "qqq-frontend-material-dashboard.js",

View File

@ -14,16 +14,18 @@ Coded by www.creative-tim.com
*/ */
import React, { import React, {
useState, useState,
useEffect, useEffect,
JSXElementConstructor, JSXElementConstructor,
Key, Key,
ReactElement, ReactElement,
useReducer, useReducer,
} from "react"; } from "react";
// react-router components // react-router components
import { Routes, Route, Navigate, useLocation } from "react-router-dom"; import {
Routes, Route, Navigate, useLocation,
} from "react-router-dom";
// @mui material components // @mui material components
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
@ -58,128 +60,137 @@ import EntityView from "./qqq/pages/entity-view";
import EntityEdit from "./qqq/pages/entity-edit"; import EntityEdit from "./qqq/pages/entity-edit";
import ProcessRun from "./qqq/pages/process-run"; import ProcessRun from "./qqq/pages/process-run";
export default function App() { export default function App()
const [controller, dispatch] = useMaterialUIController(); {
const { const [controller, dispatch] = useMaterialUIController();
miniSidenav, const {
direction, miniSidenav,
layout, direction,
openConfigurator, layout,
sidenavColor, openConfigurator,
transparentSidenav, sidenavColor,
whiteSidenav, transparentSidenav,
darkMode, whiteSidenav,
} = controller; darkMode,
const [onMouseEnter, setOnMouseEnter] = useState(false); } = controller;
const { pathname } = useLocation(); const [onMouseEnter, setOnMouseEnter] = useState(false);
const { pathname } = useLocation();
// Open sidenav when mouse enter on mini sidenav // Open sidenav when mouse enter on mini sidenav
const handleOnMouseEnter = () => { const handleOnMouseEnter = () =>
if (miniSidenav && !onMouseEnter) { {
setMiniSidenav(dispatch, false); if (miniSidenav && !onMouseEnter)
setOnMouseEnter(true); {
} setMiniSidenav(dispatch, false);
}; setOnMouseEnter(true);
}
};
// Close sidenav when mouse leave mini sidenav // Close sidenav when mouse leave mini sidenav
const handleOnMouseLeave = () => { const handleOnMouseLeave = () =>
if (onMouseEnter) { {
setMiniSidenav(dispatch, true); if (onMouseEnter)
setOnMouseEnter(false); {
} setMiniSidenav(dispatch, true);
}; setOnMouseEnter(false);
}
};
// Change the openConfigurator state // Change the openConfigurator state
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator); const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
// Setting the dir attribute for the body element // Setting the dir attribute for the body element
useEffect(() => { useEffect(() =>
document.body.setAttribute("dir", direction); {
}, [direction]); document.body.setAttribute("dir", direction);
}, [direction]);
// Setting page scroll to 0 when changing the route // Setting page scroll to 0 when changing the route
useEffect(() => { useEffect(() =>
document.documentElement.scrollTop = 0; {
document.scrollingElement.scrollTop = 0; document.documentElement.scrollTop = 0;
}, [pathname]); document.scrollingElement.scrollTop = 0;
}, [pathname]);
const getRoutes = (allRoutes: any[]): any => const getRoutes = (allRoutes: any[]): any => allRoutes.map(
allRoutes.map(
(route: { (route: {
collapse: any; collapse: any;
route: string; route: string;
component: ReactElement<any, string | JSXElementConstructor<any>>; component: ReactElement<any, string | JSXElementConstructor<any>>;
key: Key; key: Key;
}) => { }) =>
if (route.collapse) { {
return getRoutes(route.collapse); if (route.collapse)
} {
return getRoutes(route.collapse);
}
if (route.route) { if (route.route)
return <Route path={route.route} element={route.component} key={route.key} />; {
} return <Route path={route.route} element={route.component} key={route.key} />;
}
return null; return null;
} },
); );
const configsButton = ( const configsButton = (
<MDBox <MDBox
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
width="3.25rem" width="3.25rem"
height="3.25rem" height="3.25rem"
bgColor="white" bgColor="white"
shadow="sm" shadow="sm"
borderRadius="50%" borderRadius="50%"
position="fixed" position="fixed"
right="2rem" right="2rem"
bottom="2rem" bottom="2rem"
zIndex={99} zIndex={99}
color="dark" color="dark"
sx={{ cursor: "pointer" }} sx={{ cursor: "pointer" }}
onClick={handleConfiguratorOpen} onClick={handleConfiguratorOpen}
> >
<Icon fontSize="small" color="inherit"> <Icon fontSize="small" color="inherit">
settings settings
</Icon> </Icon>
</MDBox> </MDBox>
); );
const entityListElement = <EntityList />; const entityListElement = <EntityList />;
const entityCreateElement = <EntityCreate />; const entityCreateElement = <EntityCreate />;
const entityViewElement = <EntityView />; const entityViewElement = <EntityView />;
const entityEditElement = <EntityEdit />; const entityEditElement = <EntityEdit />;
const processRunElement = <ProcessRun />; const processRunElement = <ProcessRun />;
return ( return (
<ThemeProvider theme={darkMode ? themeDark : theme}> <ThemeProvider theme={darkMode ? themeDark : theme}>
<CssBaseline /> <CssBaseline />
{layout === "dashboard" && ( {layout === "dashboard" && (
<> <>
<Sidenav <Sidenav
color={sidenavColor} color={sidenavColor}
brand={(transparentSidenav && !darkMode) || whiteSidenav ? brandDark : brandWhite} brand={(transparentSidenav && !darkMode) || whiteSidenav ? brandDark : brandWhite}
brandName="Material Dashboard PRO" brandName="Material Dashboard PRO"
routes={routes} routes={routes}
onMouseEnter={handleOnMouseEnter} onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave} onMouseLeave={handleOnMouseLeave}
/> />
<Configurator /> <Configurator />
{configsButton} {configsButton}
</> </>
)} )}
{layout === "vr" && <Configurator />} {layout === "vr" && <Configurator />}
<Routes> <Routes>
<Route path="*" element={<Navigate to="/dashboards/analytics" />} /> <Route path="*" element={<Navigate to="/dashboards/analytics" />} />
<Route path="/:tableName" element={entityListElement} key="entity-list" />; <Route path="/:tableName" element={entityListElement} key="entity-list" />
<Route path="/:tableName/create" element={entityCreateElement} key="entity-create" />; <Route path="/:tableName/create" element={entityCreateElement} key="entity-create" />
<Route path="/processes/:processName" element={processRunElement} key="process-run" />; <Route path="/processes/:processName" element={processRunElement} key="process-run" />
<Route path="/:tableName/:id" element={entityViewElement} key="entity-view" />; <Route path="/:tableName/:id" element={entityViewElement} key="entity-view" />
<Route path="/:tableName/:id/edit" element={entityEditElement} key="entity-edit" />; <Route path="/:tableName/:id/edit" element={entityEditElement} key="entity-edit" />
{getRoutes(routes)} {getRoutes(routes)}
</Routes> </Routes>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@ -21,12 +21,12 @@ import App from "App";
import { MaterialUIControllerProvider } from "context"; import { MaterialUIControllerProvider } from "context";
ReactDOM.render( ReactDOM.render(
<BrowserRouter> <BrowserRouter>
<MaterialUIControllerProvider> <MaterialUIControllerProvider>
<App /> <App />
</MaterialUIControllerProvider> </MaterialUIControllerProvider>
</BrowserRouter>, </BrowserRouter>,
document.getElementById("root") document.getElementById("root"),
); );
export * from "components/MDButton"; export * from "components/MDButton";

View File

@ -39,235 +39,235 @@ Coded by www.creative-tim.com
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
const pageRoutes = [ const pageRoutes = [
{ {
name: "pages", name: "pages",
columns: 3, columns: 3,
rowsPerColumn: 2, rowsPerColumn: 2,
collapse: [ collapse: [
{ {
name: "dashboards", name: "dashboards",
icon: <Icon>dashboard</Icon>, icon: <Icon>dashboard</Icon>,
collapse: [ collapse: [
{ {
name: "analytics", name: "analytics",
route: "/dashboards/analytics", route: "/dashboards/analytics",
}, },
{ {
name: "sales", name: "sales",
route: "/dashboards/sales", route: "/dashboards/sales",
}, },
], ],
}, },
{ {
name: "users", name: "users",
icon: <Icon>people</Icon>, icon: <Icon>people</Icon>,
collapse: [ collapse: [
{ {
name: "reports", name: "reports",
route: "/pages/users/reports", route: "/pages/users/reports",
}, },
], ],
}, },
{ {
name: "extra", name: "extra",
icon: <Icon>queue_play_next</Icon>, icon: <Icon>queue_play_next</Icon>,
collapse: [ collapse: [
{ {
name: "pricing page", name: "pricing page",
route: "/pages/pricing-page", route: "/pages/pricing-page",
}, },
{ name: "RTL", route: "/pages/rtl" }, { name: "RTL", route: "/pages/rtl" },
{ name: "widgets", route: "/pages/widgets" }, { name: "widgets", route: "/pages/widgets" },
{ name: "charts", route: "/pages/charts" }, { name: "charts", route: "/pages/charts" },
{ {
name: "notfications", name: "notfications",
route: "/pages/notifications", route: "/pages/notifications",
}, },
], ],
}, },
{ {
name: "projects", name: "projects",
icon: <Icon>precision_manufacturing</Icon>, icon: <Icon>precision_manufacturing</Icon>,
collapse: [ collapse: [
{ {
name: "timeline", name: "timeline",
route: "/pages/projects/timeline", route: "/pages/projects/timeline",
}, },
], ],
}, },
{ {
name: "account", name: "account",
icon: <Icon>account_balance</Icon>, icon: <Icon>account_balance</Icon>,
collapse: [ collapse: [
{ {
name: "settings", name: "settings",
route: "/pages/account/setting", route: "/pages/account/setting",
}, },
{ {
name: "billing", name: "billing",
route: "/pages/account/billing", route: "/pages/account/billing",
}, },
{ {
name: "invoice", name: "invoice",
route: "/pages/account/invoice", route: "/pages/account/invoice",
}, },
], ],
}, },
{ {
name: "profile", name: "profile",
icon: <Icon>badge</Icon>, icon: <Icon>badge</Icon>,
collapse: [ collapse: [
{ {
name: "profile overview", name: "profile overview",
route: "/pages/profile/profile-overview", route: "/pages/profile/profile-overview",
}, },
{ {
name: "all projects", name: "all projects",
route: "/pages/profile/all-projects", route: "/pages/profile/all-projects",
}, },
], ],
}, },
], ],
}, },
{ {
name: "authenticaton", name: "authenticaton",
collapse: [ collapse: [
{ {
name: "sign in", name: "sign in",
dropdown: true, dropdown: true,
icon: <Icon>login</Icon>, icon: <Icon>login</Icon>,
collapse: [ collapse: [
{ {
name: "basic", name: "basic",
route: "/authentication/sign-in/basic", route: "/authentication/sign-in/basic",
}, },
{ {
name: "cover", name: "cover",
route: "/authentication/sign-in/cover", route: "/authentication/sign-in/cover",
}, },
{ {
name: "illustration", name: "illustration",
route: "/authentication/sign-in/illustration", route: "/authentication/sign-in/illustration",
}, },
], ],
}, },
{ {
name: "sign up", name: "sign up",
dropdown: true, dropdown: true,
icon: <Icon>assignment</Icon>, icon: <Icon>assignment</Icon>,
collapse: [ collapse: [
{ {
name: "cover", name: "cover",
route: "/authentication/sign-up/cover", route: "/authentication/sign-up/cover",
}, },
], ],
}, },
{ {
name: "reset password", name: "reset password",
dropdown: true, dropdown: true,
icon: <Icon>restart_alt</Icon>, icon: <Icon>restart_alt</Icon>,
collapse: [ collapse: [
{ {
name: "cover", name: "cover",
route: "/authentication/reset-password/cover", route: "/authentication/reset-password/cover",
}, },
], ],
}, },
], ],
}, },
{ {
name: "application", name: "application",
collapse: [ collapse: [
{ {
name: "kanban", name: "kanban",
route: "/applications/kanban", route: "/applications/kanban",
icon: "widgets", icon: "widgets",
}, },
{ {
name: "wizard", name: "wizard",
route: "/applications/wizard", route: "/applications/wizard",
icon: "import_contacts", icon: "import_contacts",
}, },
{ {
name: "data tables", name: "data tables",
route: "/applications/data-tables", route: "/applications/data-tables",
icon: "backup_table", icon: "backup_table",
}, },
{ {
name: "calendar", name: "calendar",
route: "/applications/calendar", route: "/applications/calendar",
icon: "event", icon: "event",
}, },
], ],
}, },
{ {
name: "ecommerce", name: "ecommerce",
columns: 2, columns: 2,
rowsPerColumn: 1, rowsPerColumn: 1,
collapse: [ collapse: [
{ {
name: "orders", name: "orders",
icon: <Icon>shopping_cart</Icon>, icon: <Icon>shopping_cart</Icon>,
collapse: [ collapse: [
{ {
name: "order list", name: "order list",
route: "/ecommerce/orders/order-list", route: "/ecommerce/orders/order-list",
}, },
{ {
name: "order details", name: "order details",
route: "/ecommerce/orders/order-details", route: "/ecommerce/orders/order-details",
}, },
], ],
}, },
{ {
name: "products", name: "products",
icon: <Icon>memory</Icon>, icon: <Icon>memory</Icon>,
collapse: [ collapse: [
{ {
name: "new product", name: "new product",
route: "/ecommerce/products/new-product", route: "/ecommerce/products/new-product",
}, },
{ {
name: "edit product", name: "edit product",
route: "/ecommerce/products/edit-product", route: "/ecommerce/products/edit-product",
}, },
{ {
name: "product page", name: "product page",
route: "/ecommerce/products/product-page", route: "/ecommerce/products/product-page",
}, },
], ],
}, },
], ],
}, },
{ {
name: "docs", name: "docs",
collapse: [ collapse: [
{ {
name: "getting started", name: "getting started",
href: "https://www.creative-tim.com/learning-lab/react/quick-start/material-dashboard/", href: "https://www.creative-tim.com/learning-lab/react/quick-start/material-dashboard/",
description: "All about overview, quick start, license and contents", description: "All about overview, quick start, license and contents",
icon: <Icon>article</Icon>, icon: <Icon>article</Icon>,
}, },
{ {
name: "foundation", name: "foundation",
href: "https://www.creative-tim.com/learning-lab/react/colors/material-dashboard/", href: "https://www.creative-tim.com/learning-lab/react/colors/material-dashboard/",
description: "See our colors, icons and typography", description: "See our colors, icons and typography",
icon: <Icon>grading</Icon>, icon: <Icon>grading</Icon>,
}, },
{ {
name: "components", name: "components",
href: "https://www.creative-tim.com/learning-lab/react/alerts/material-dashboard/", href: "https://www.creative-tim.com/learning-lab/react/alerts/material-dashboard/",
description: "Explore our collection of fully designed components", description: "Explore our collection of fully designed components",
icon: <Icon>apps</Icon>, icon: <Icon>apps</Icon>,
}, },
{ {
name: "plugins", name: "plugins",
href: "https://www.creative-tim.com/learning-lab/react/datepicker/material-dashboard/", href: "https://www.creative-tim.com/learning-lab/react/datepicker/material-dashboard/",
description: "Check how you can integrate our plugins", description: "Check how you can integrate our plugins",
icon: <Icon>extension</Icon>, icon: <Icon>extension</Icon>,
}, },
], ],
}, },
]; ];
export default pageRoutes; export default pageRoutes;

View File

@ -30,41 +30,44 @@ interface Props {
children: ReactNode; children: ReactNode;
} }
function BaseLayout({ stickyNavbar, children }: Props): JSX.Element { function BaseLayout({ stickyNavbar, children }: Props): JSX.Element
const [tabsOrientation, setTabsOrientation] = useState<"horizontal" | "vertical">("horizontal"); {
const [tabsOrientation, setTabsOrientation] = useState<"horizontal" | "vertical">("horizontal");
useEffect(() => { useEffect(() =>
// A function that sets the orientation state of the tabs. {
function handleTabsOrientation() { // A function that sets the orientation state of the tabs.
return window.innerWidth < breakpoints.values.sm function handleTabsOrientation()
? setTabsOrientation("vertical") {
: setTabsOrientation("horizontal"); return window.innerWidth < breakpoints.values.sm
} ? setTabsOrientation("vertical")
: setTabsOrientation("horizontal");
}
/** /**
The event listener that's calling the handleTabsOrientation function when resizing the window. The event listener that's calling the handleTabsOrientation function when resizing the window.
*/ */
window.addEventListener("resize", handleTabsOrientation); window.addEventListener("resize", handleTabsOrientation);
// Call the handleTabsOrientation function to set the state with the initial value. // Call the handleTabsOrientation function to set the state with the initial value.
handleTabsOrientation(); handleTabsOrientation();
// Remove event listener on cleanup // Remove event listener on cleanup
return () => window.removeEventListener("resize", handleTabsOrientation); return () => window.removeEventListener("resize", handleTabsOrientation);
}, [tabsOrientation]); }, [tabsOrientation]);
return ( return (
<DashboardLayout> <DashboardLayout>
<DashboardNavbar absolute={!stickyNavbar} isMini /> <DashboardNavbar absolute={!stickyNavbar} isMini />
<MDBox mt={stickyNavbar ? 3 : 10}>{children}</MDBox> <MDBox mt={stickyNavbar ? 3 : 10}>{children}</MDBox>
<Footer /> <Footer />
</DashboardLayout> </DashboardLayout>
); );
} }
// Declaring default props for BaseLayout // Declaring default props for BaseLayout
BaseLayout.defaultProps = { BaseLayout.defaultProps = {
stickyNavbar: false, stickyNavbar: false,
}; };
export default BaseLayout; export default BaseLayout;

View File

@ -34,23 +34,24 @@ interface Props {
| "dark"; | "dark";
} }
function CustomerCell({ image, name, color }: Props): JSX.Element { function CustomerCell({ image, name, color }: Props): JSX.Element
return ( {
<MDBox display="flex" alignItems="center"> return (
<MDBox mr={1}> <MDBox display="flex" alignItems="center">
<MDAvatar bgColor={color} src={image} alt={name} size="xs" /> <MDBox mr={1}>
<MDAvatar bgColor={color} src={image} alt={name} size="xs" />
</MDBox>
<MDTypography variant="caption" fontWeight="medium" color="text" sx={{ lineHeight: 0 }}>
{name}
</MDTypography>
</MDBox> </MDBox>
<MDTypography variant="caption" fontWeight="medium" color="text" sx={{ lineHeight: 0 }}> );
{name}
</MDTypography>
</MDBox>
);
} }
// Declaring default props for CustomerCell // Declaring default props for CustomerCell
CustomerCell.defaultProps = { CustomerCell.defaultProps = {
image: "", image: "",
color: "dark", color: "dark",
}; };
export default CustomerCell; export default CustomerCell;

View File

@ -22,22 +22,24 @@ interface Props {
suffix?: string | boolean; suffix?: string | boolean;
} }
function DefaultCell({ value, suffix }: Props): JSX.Element { function DefaultCell({ value, suffix }: Props): JSX.Element
return ( {
<MDTypography variant="caption" fontWeight="medium" color="text"> return (
{value} <MDTypography variant="caption" fontWeight="medium" color="text">
{suffix && ( {value}
<MDTypography variant="caption" fontWeight="medium" color="secondary"> {suffix && (
&nbsp;&nbsp;{suffix} <MDTypography variant="caption" fontWeight="medium" color="secondary">
</MDTypography> &nbsp;&nbsp;
)} {suffix}
</MDTypography> </MDTypography>
); )}
</MDTypography>
);
} }
// Declaring default props for DefaultCell // Declaring default props for DefaultCell
DefaultCell.defaultProps = { DefaultCell.defaultProps = {
suffix: "", suffix: "",
}; };
export default DefaultCell; export default DefaultCell;

View File

@ -27,27 +27,28 @@ interface Props {
checked?: boolean; checked?: boolean;
} }
function IdCell({ id, checked }: Props): JSX.Element { function IdCell({ id, checked }: Props): JSX.Element
const pathParts = window.location.pathname.split("/"); {
const tableName = pathParts[1]; const pathParts = window.location.pathname.split("/");
const href = `/${tableName}/${id}`; const tableName = pathParts[1];
const link = <Link href={href}>{id}</Link>; const href = `/${tableName}/${id}`;
const link = <Link href={href}>{id}</Link>;
return ( return (
<MDBox display="flex" alignItems="center"> <MDBox display="flex" alignItems="center">
<Checkbox defaultChecked={checked} /> <Checkbox defaultChecked={checked} />
<MDBox ml={1}> <MDBox ml={1}>
<MDTypography variant="caption" fontWeight="medium" color="text"> <MDTypography variant="caption" fontWeight="medium" color="text">
{link} {link}
</MDTypography> </MDTypography>
</MDBox>
</MDBox> </MDBox>
</MDBox> );
);
} }
// Declaring default props for IdCell // Declaring default props for IdCell
IdCell.defaultProps = { IdCell.defaultProps = {
checked: false, checked: false,
}; };
export default IdCell; export default IdCell;

View File

@ -38,19 +38,20 @@ interface Props {
status: string; status: string;
} }
function StatusCell({ icon, color, status }: Props): JSX.Element { function StatusCell({ icon, color, status }: Props): JSX.Element
return ( {
<MDBox display="flex" alignItems="center"> return (
<MDBox mr={1}> <MDBox display="flex" alignItems="center">
<MDButton variant="outlined" color={color} size="small" iconOnly circular> <MDBox mr={1}>
<Icon sx={{ fontWeight: "bold" }}>{icon}</Icon> <MDButton variant="outlined" color={color} size="small" iconOnly circular>
</MDButton> <Icon sx={{ fontWeight: "bold" }}>{icon}</Icon>
</MDButton>
</MDBox>
<MDTypography variant="caption" fontWeight="medium" color="text" sx={{ lineHeight: 0 }}>
{status}
</MDTypography>
</MDBox> </MDBox>
<MDTypography variant="caption" fontWeight="medium" color="text" sx={{ lineHeight: 0 }}> );
{status}
</MDTypography>
</MDBox>
);
} }
export default StatusCell; export default StatusCell;

View File

@ -14,180 +14,180 @@ Coded by www.creative-tim.com
*/ */
const selectData = { const selectData = {
gender: ["Male", "Female"], gender: ["Male", "Female"],
birthDate: [ birthDate: [
"January", "January",
"February", "February",
"March", "March",
"April", "April",
"May", "May",
"June", "June",
"July", "July",
"August", "August",
"September", "September",
"October", "October",
"November", "November",
"December", "December",
], ],
days: [ days: [
"1", "1",
"2", "2",
"3", "3",
"4", "4",
"5", "5",
"6", "6",
"7", "7",
"8", "8",
"9", "9",
"10", "10",
"11", "11",
"12", "12",
"13", "13",
"14", "14",
"15", "15",
"16", "16",
"17", "17",
"18", "18",
"19", "19",
"20", "20",
"21", "21",
"22", "22",
"23", "23",
"24", "24",
"25", "25",
"26", "26",
"27", "27",
"28", "28",
"29", "29",
"30", "30",
], ],
years: [ years: [
"1900", "1900",
"1901", "1901",
"1902", "1902",
"1903", "1903",
"1904", "1904",
"1905", "1905",
"1906", "1906",
"1907", "1907",
"1908", "1908",
"1909", "1909",
"1910", "1910",
"1911", "1911",
"1912", "1912",
"1913", "1913",
"1914", "1914",
"1915", "1915",
"1915", "1915",
"1915", "1915",
"1916", "1916",
"1917", "1917",
"1918", "1918",
"1919", "1919",
"1920", "1920",
"1921", "1921",
"1922", "1922",
"1923", "1923",
"1924", "1924",
"1925", "1925",
"1926", "1926",
"1927", "1927",
"1928", "1928",
"1929", "1929",
"1930", "1930",
"1931", "1931",
"1932", "1932",
"1933", "1933",
"1934", "1934",
"1935", "1935",
"1936", "1936",
"1937", "1937",
"1938", "1938",
"1939", "1939",
"1940", "1940",
"1941", "1941",
"1942", "1942",
"1943", "1943",
"1944", "1944",
"1945", "1945",
"1946", "1946",
"1947", "1947",
"1948", "1948",
"1949", "1949",
"1950", "1950",
"1951", "1951",
"1952", "1952",
"1953", "1953",
"1954", "1954",
"1955", "1955",
"1956", "1956",
"1957", "1957",
"1958", "1958",
"1959", "1959",
"1960", "1960",
"1961", "1961",
"1962", "1962",
"1963", "1963",
"1964", "1964",
"1965", "1965",
"1966", "1966",
"1967", "1967",
"1968", "1968",
"1969", "1969",
"1970", "1970",
"1971", "1971",
"1972", "1972",
"1973", "1973",
"1974", "1974",
"1975", "1975",
"1976", "1976",
"1977", "1977",
"1978", "1978",
"1979", "1979",
"1980", "1980",
"1981", "1981",
"1982", "1982",
"1983", "1983",
"1984", "1984",
"1985", "1985",
"1986", "1986",
"1987", "1987",
"1988", "1988",
"1989", "1989",
"1990", "1990",
"1991", "1991",
"1992", "1992",
"1993", "1993",
"1994", "1994",
"1995", "1995",
"1996", "1996",
"1997", "1997",
"1998", "1998",
"1999", "1999",
"2000", "2000",
"2001", "2001",
"2002", "2002",
"2003", "2003",
"2004", "2004",
"2005", "2005",
"2006", "2006",
"2007", "2007",
"2008", "2008",
"2009", "2009",
"2010", "2010",
"2011", "2011",
"2012", "2012",
"2013", "2013",
"2014", "2014",
"2015", "2015",
"2016", "2016",
"2017", "2017",
"2018", "2018",
"2019", "2019",
"2020", "2020",
"2021", "2021",
], ],
skills: ["react", "vue", "angular", "svelte", "javascript"], skills: ["react", "vue", "angular", "svelte", "javascript"],
}; };
export default selectData; export default selectData;

View File

@ -28,153 +28,174 @@ interface Props {
id?: string; id?: string;
} }
function EntityForm({ id }: Props): JSX.Element { function EntityForm({ id }: Props): JSX.Element
const qController = new QController(""); {
const { tableName } = useParams(); const qController = new QController("");
const { tableName } = useParams();
const [validations, setValidations] = useState({}); const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); const [initialValues, setInitialValues] = useState({} as { [key: string]: string });
const [formFields, setFormFields] = useState({}); const [formFields, setFormFields] = useState({});
const [alertContent, setAlertContent] = useState(""); const [alertContent, setAlertContent] = useState("");
const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [formValues, setFormValues] = useState({} as { [key: string]: string }); const [formValues, setFormValues] = useState({} as { [key: string]: string });
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
function getDynamicStepContent(formData: any): JSX.Element { function getDynamicStepContent(formData: any): JSX.Element
const { formFields, values, errors, touched } = formData; {
const {
formFields, values, errors, touched,
} = formData;
if (!Object.keys(formFields).length) { if (!Object.keys(formFields).length)
return <div>Loading...</div>; {
} return <div>Loading...</div>;
return <QDynamicForm formData={formData} primaryKeyId={tableMetaData.primaryKeyField} />;
}
if (!asyncLoadInited) {
setAsyncLoadInited(true);
(async () => {
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
const fieldArray = [] as QFieldMetaData[];
const sortedKeys = [...tableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) => {
const fieldMetaData = tableMetaData.fields.get(key);
fieldArray.push(fieldMetaData);
});
if (id !== null) {
const record = await qController.get(tableName, id);
tableMetaData.fields.forEach((fieldMetaData, key) => {
initialValues[key] = record.values.get(key);
});
setFormValues(formValues);
} }
const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(fieldArray); return <QDynamicForm formData={formData} primaryKeyId={tableMetaData.primaryKeyField} />;
setInitialValues(initialValues); }
setFormFields(dynamicFormFields);
setValidations(Yup.object().shape(formValidations));
forceUpdate(); if (!asyncLoadInited)
})(); {
} setAsyncLoadInited(true);
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
const handleSubmit = async (values: any, actions: any) => { const fieldArray = [] as QFieldMetaData[];
actions.setSubmitting(true); const sortedKeys = [...tableMetaData.fields.keys()].sort();
await (async () => { sortedKeys.forEach((key) =>
if (id !== null) { {
await qController const fieldMetaData = tableMetaData.fields.get(key);
.update(tableName, id, values) fieldArray.push(fieldMetaData);
.then((record) => { });
window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField
)}`;
})
.catch((error) => {
setAlertContent(error.response.data.error);
});
} else {
await qController
.create(tableName, values)
.then((record) => {
window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField
)}`;
})
.catch((error) => {
setAlertContent(error.response.data.error);
});
}
})();
};
const formTitle = if (id !== null)
id != null ? `Edit ${tableMetaData?.label} (${id})` : `Create New ${tableMetaData?.label}`; {
const formId = const record = await qController.get(tableName, id);
id != null ? `edit-${tableMetaData?.label}-form` : `create-${tableMetaData?.label}-form`;
return ( tableMetaData.fields.forEach((fieldMetaData, key) =>
<MDBox mb={3}> {
<Grid container spacing={3}> initialValues[key] = record.values.get(key);
<Grid item xs={12}> });
{alertContent ? (
<MDBox mb={3}> setFormValues(formValues);
<Alert severity="error">{alertContent}</Alert> }
</MDBox>
) : ( const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(fieldArray);
"" setInitialValues(initialValues);
)} setFormFields(dynamicFormFields);
<Card id="edit-form-container" sx={{ overflow: "visible" }}> setValidations(Yup.object().shape(formValidations));
<MDBox p={3}>
<MDTypography variant="h5">{formTitle}</MDTypography> forceUpdate();
</MDBox> })();
<MDBox pb={3} px={3}> }
<Grid key="fields-grid" container spacing={3}>
<Formik const handleSubmit = async (values: any, actions: any) =>
initialValues={initialValues} {
validationSchema={validations} actions.setSubmitting(true);
onSubmit={handleSubmit} await (async () =>
> {
{({ values, errors, touched, isSubmitting }) => ( if (id !== null)
<Form id={formId} autoComplete="off"> {
<MDBox p={3} width="100%"> await qController
{/*************************************************************************** .update(tableName, id, values)
.then((record) =>
{
window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField,
)}`;
})
.catch((error) =>
{
setAlertContent(error.response.data.error);
});
}
else
{
await qController
.create(tableName, values)
.then((record) =>
{
window.location.href = `/${tableName}/${record.values.get(
tableMetaData.primaryKeyField,
)}`;
})
.catch((error) =>
{
setAlertContent(error.response.data.error);
});
}
})();
};
const formTitle = id != null ? `Edit ${tableMetaData?.label} (${id})` : `Create New ${tableMetaData?.label}`;
const formId = id != null ? `edit-${tableMetaData?.label}-form` : `create-${tableMetaData?.label}-form`;
return (
<MDBox mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
{alertContent ? (
<MDBox mb={3}>
<Alert severity="error">{alertContent}</Alert>
</MDBox>
) : (
""
)}
<Card id="edit-form-container" sx={{ overflow: "visible" }}>
<MDBox p={3}>
<MDTypography variant="h5">{formTitle}</MDTypography>
</MDBox>
<MDBox pb={3} px={3}>
<Grid key="fields-grid" container spacing={3}>
<Formik
initialValues={initialValues}
validationSchema={validations}
onSubmit={handleSubmit}
>
{({
values, errors, touched, isSubmitting,
}) => (
<Form id={formId} autoComplete="off">
<MDBox p={3} width="100%">
{/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step ** ** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/} ***************************************************************************/}
{getDynamicStepContent({ {getDynamicStepContent({
values, values,
touched, touched,
formFields, formFields,
errors, errors,
})} })}
<Grid key="buttonGrid" container spacing={3}> <Grid key="buttonGrid" container spacing={3}>
<MDBox mt={5} ml="auto"> <MDBox mt={5} ml="auto">
<MDButton type="submit" variant="gradient" color="dark" size="small"> <MDButton type="submit" variant="gradient" color="dark" size="small">
save {tableMetaData?.label} save
</MDButton> {" "}
</MDBox> {tableMetaData?.label}
</Grid> </MDButton>
</MDBox> </MDBox>
</Form> </Grid>
)} </MDBox>
</Formik> </Form>
</Grid> )}
</MDBox> </Formik>
</Card> </Grid>
</Grid> </MDBox>
</Grid> </Card>
</MDBox> </Grid>
); </Grid>
</MDBox>
);
} }
// Declaring default props for DefaultCell // Declaring default props for DefaultCell
EntityForm.defaultProps = { EntityForm.defaultProps = {
id: null, id: null,
}; };
export default EntityForm; export default EntityForm;

View File

@ -37,80 +37,85 @@ interface Props {
[key: string]: any; [key: string]: any;
} }
function Footer({ company, links }: Props): JSX.Element { function Footer({ company, links }: Props): JSX.Element
const { href, name } = company; {
const { size } = typography; const { href, name } = company;
const { size } = typography;
const renderLinks = () => const renderLinks = () => links.map((link) => (
links.map((link) => (
<MDBox key={link.name} component="li" px={2} lineHeight={1}> <MDBox key={link.name} component="li" px={2} lineHeight={1}>
<Link href={link.href} target="_blank"> <Link href={link.href} target="_blank">
<MDTypography variant="button" fontWeight="regular" color="text"> <MDTypography variant="button" fontWeight="regular" color="text">
{link.name} {link.name}
</MDTypography> </MDTypography>
</Link> </Link>
</MDBox> </MDBox>
)); ));
return ( return (
<MDBox
width="100%"
display="flex"
flexDirection={{ xs: "column", lg: "row" }}
justifyContent="space-between"
alignItems="center"
px={1.5}
>
<MDBox <MDBox
display="flex" width="100%"
justifyContent="center" display="flex"
alignItems="center" flexDirection={{ xs: "column", lg: "row" }}
flexWrap="wrap" justifyContent="space-between"
color="text" alignItems="center"
fontSize={size.sm} px={1.5}
px={1.5}
> >
&copy; {new Date().getFullYear()}, made with <MDBox
<MDBox fontSize={size.md} color="text" mb={-0.5} mx={0.25}> display="flex"
<Icon color="inherit" fontSize="inherit"> justifyContent="center"
favorite alignItems="center"
</Icon> flexWrap="wrap"
</MDBox> color="text"
by fontSize={size.sm}
<Link href={href} target="_blank"> px={1.5}
<MDTypography variant="button" fontWeight="medium"> >
&nbsp;{name}&nbsp; &copy;
</MDTypography> {" "}
</Link> {new Date().getFullYear()}
for a better web. , made with
</MDBox> <MDBox fontSize={size.md} color="text" mb={-0.5} mx={0.25}>
<MDBox <Icon color="inherit" fontSize="inherit">
component="ul" favorite
sx={({ breakpoints }) => ({ </Icon>
display: "flex", </MDBox>
flexWrap: "wrap", by
alignItems: "center", <Link href={href} target="_blank">
justifyContent: "center", <MDTypography variant="button" fontWeight="medium">
listStyle: "none", &nbsp;
mt: 3, {name}
mb: 0, &nbsp;
p: 0, </MDTypography>
</Link>
for a better web.
</MDBox>
<MDBox
component="ul"
sx={({ breakpoints }) => ({
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
listStyle: "none",
mt: 3,
mb: 0,
p: 0,
[breakpoints.up("lg")]: { [breakpoints.up("lg")]: {
mt: 0, mt: 0,
}, },
})} })}
> >
{renderLinks()} {renderLinks()}
</MDBox>
</MDBox> </MDBox>
</MDBox> );
);
} }
// Declaring default props for Footer // Declaring default props for Footer
Footer.defaultProps = { Footer.defaultProps = {
company: { href: "https://www.kingsrook.com/", name: "Kingsrook" }, company: { href: "https://www.kingsrook.com/", name: "Kingsrook" },
links: [{ href: "https://www.kingsrook.com/", name: "Kingsrook" }], links: [{ href: "https://www.kingsrook.com/", name: "Kingsrook" }],
}; };
export default Footer; export default Footer;

View File

@ -30,11 +30,14 @@ interface Props {
primaryKeyId?: string; primaryKeyId?: string;
} }
function QDynamicForm(props: Props): JSX.Element { function QDynamicForm(props: Props): JSX.Element
const { formData, formLabel, primaryKeyId } = props; {
const { formFields, values, errors, touched } = formData; const { formData, formLabel, primaryKeyId } = props;
const {
formFields, values, errors, touched,
} = formData;
/* /*
const { const {
firstName: firstNameV, firstName: firstNameV,
lastName: lastNameV, lastName: lastNameV,
@ -45,42 +48,45 @@ function QDynamicForm(props: Props): JSX.Element {
} = values; } = values;
*/ */
return ( return (
<MDBox> <MDBox>
<MDBox lineHeight={0}> <MDBox lineHeight={0}>
<MDTypography variant="h5">{formLabel}</MDTypography> <MDTypography variant="h5">{formLabel}</MDTypography>
{/* TODO - help text {/* TODO - help text
<MDTypography variant="button" color="text"> <MDTypography variant="button" color="text">
Mandatory information Mandatory information
</MDTypography> </MDTypography>
*/} */}
</MDBox> </MDBox>
<MDBox mt={1.625}> <MDBox mt={1.625}>
<Grid container spacing={3}> <Grid container spacing={3}>
{formFields && {formFields
Object.keys(formFields).length > 0 && && Object.keys(formFields).length > 0
Object.keys(formFields).map((fieldName: any) => { && Object.keys(formFields).map((fieldName: any) =>
const field = formFields[fieldName]; {
if (primaryKeyId && fieldName === primaryKeyId) { const field = formFields[fieldName];
return null; if (primaryKeyId && fieldName === primaryKeyId)
} {
if (values[fieldName] === undefined) { return null;
values[fieldName] = ""; }
} if (values[fieldName] === undefined)
return ( {
<Grid item xs={12} sm={6} key={fieldName}> values[fieldName] = "";
<FormField }
type={field.type} return (
label={field.label} <Grid item xs={12} sm={6} key={fieldName}>
name={fieldName} <FormField
value={values[fieldName]} type={field.type}
error={errors[fieldName] && touched[fieldName]} label={field.label}
/> name={fieldName}
</Grid> value={values[fieldName]}
); error={errors[fieldName] && touched[fieldName]}
/>
</Grid>
);
})} })}
</Grid> </Grid>
{/* {/*
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<FormField <FormField
@ -154,14 +160,14 @@ function QDynamicForm(props: Props): JSX.Element {
</Grid> </Grid>
</Grid> </Grid>
*/} */}
</MDBox>
</MDBox> </MDBox>
</MDBox> );
);
} }
QDynamicForm.defaultProps = { QDynamicForm.defaultProps = {
formLabel: undefined, formLabel: undefined,
primaryKeyId: undefined, primaryKeyId: undefined,
}; };
export default QDynamicForm; export default QDynamicForm;

View File

@ -30,51 +30,56 @@ import { QFieldType } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFie
** Meta-data to represent a single field in a table. ** Meta-data to represent a single field in a table.
** **
*******************************************************************************/ *******************************************************************************/
class DynamicFormUtils { class DynamicFormUtils
public static getFormData(qqqFormFields: QFieldMetaData[]) { {
const dynamicFormFields: any = {}; public static getFormData(qqqFormFields: QFieldMetaData[])
const formValidations: any = {}; {
const dynamicFormFields: any = {};
const formValidations: any = {};
qqqFormFields.forEach((field) => { qqqFormFields.forEach((field) =>
let fieldType: string; {
switch (field.type.toString()) { let fieldType: string;
case QFieldType.DECIMAL: switch (field.type.toString())
case QFieldType.INTEGER: {
fieldType = "number"; case QFieldType.DECIMAL:
break; case QFieldType.INTEGER:
case QFieldType.DATE_TIME: fieldType = "number";
fieldType = "datetime-local"; break;
break; case QFieldType.DATE_TIME:
case QFieldType.PASSWORD: fieldType = "datetime-local";
case QFieldType.TIME: break;
case QFieldType.DATE: case QFieldType.PASSWORD:
fieldType = field.type.toString(); case QFieldType.TIME:
break; case QFieldType.DATE:
case QFieldType.TEXT: fieldType = field.type.toString();
case QFieldType.HTML: break;
case QFieldType.STRING: case QFieldType.TEXT:
default: case QFieldType.HTML:
fieldType = "text"; case QFieldType.STRING:
} default:
fieldType = "text";
}
let label = field.label ? field.label : field.name; let label = field.label ? field.label : field.name;
label += field.isRequired ? " *" : ""; label += field.isRequired ? " *" : "";
dynamicFormFields[field.name] = { dynamicFormFields[field.name] = {
name: field.name, name: field.name,
label: label, label: label,
isRequired: field.isRequired, isRequired: field.isRequired,
type: fieldType, type: fieldType,
// todo invalidMsg: "Zipcode is not valid (e.g. 70000).", // todo invalidMsg: "Zipcode is not valid (e.g. 70000).",
}; };
if (field.isRequired) { if (field.isRequired)
formValidations[field.name] = Yup.string().required(`${field.label} is required.`); {
} formValidations[field.name] = Yup.string().required(`${field.label} is required.`);
}); }
});
return { dynamicFormFields, formValidations }; return { dynamicFormFields, formValidations };
} }
} }
export default DynamicFormUtils; export default DynamicFormUtils;

View File

@ -23,18 +23,19 @@ import MDBox from "components/MDBox";
import EntityForm from "qqq/components/EntityForm"; import EntityForm from "qqq/components/EntityForm";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
function EntityCreate(): JSX.Element { function EntityCreate(): JSX.Element
return ( {
<BaseLayout> return (
<MDBox mt={4}> <BaseLayout>
<Grid container spacing={3}> <MDBox mt={4}>
<Grid item xs={12} lg={12}> <Grid container spacing={3}>
<EntityForm /> <Grid item xs={12} lg={12}>
</Grid> <EntityForm />
</Grid> </Grid>
</MDBox> </Grid>
</BaseLayout> </MDBox>
); </BaseLayout>
);
} }
export default EntityCreate; export default EntityCreate;

View File

@ -24,26 +24,27 @@ import BaseLayout from "qqq/components/BaseLayout";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import EntityForm from "qqq/components/EntityForm"; import EntityForm from "qqq/components/EntityForm";
function EntityEdit(): JSX.Element { function EntityEdit(): JSX.Element
const { id } = useParams(); {
const { id } = useParams();
return ( return (
<BaseLayout> <BaseLayout>
<MDBox mt={4}> <MDBox mt={4}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} lg={12}> <Grid item xs={12} lg={12}>
<MDBox mb={3}> <MDBox mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<EntityForm id={id} /> <EntityForm id={id} />
</Grid> </Grid>
</Grid> </Grid>
</MDBox> </MDBox>
</Grid> </Grid>
</Grid> </Grid>
</MDBox> </MDBox>
</BaseLayout> </BaseLayout>
); );
} }
export default EntityEdit; export default EntityEdit;

View File

@ -26,18 +26,18 @@ import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { makeStyles, Alert } from "@mui/material"; import { makeStyles, Alert } from "@mui/material";
import { import {
DataGrid, DataGrid,
GridCallbackDetails, GridCallbackDetails,
GridColDef, GridColDef,
GridColumnVisibilityModel, GridColumnVisibilityModel,
GridFilterModel, GridFilterModel,
GridRowId, GridRowId,
GridRowParams, GridRowParams,
GridRowsProp, GridRowsProp,
GridSelectionModel, GridSelectionModel,
GridSortItem, GridSortItem,
GridSortModel, GridSortModel,
GridToolbar, GridToolbar,
} from "@mui/x-data-grid"; } from "@mui/x-data-grid";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
@ -66,325 +66,359 @@ interface Props {
table?: QTableMetaData; table?: QTableMetaData;
} }
function EntityList({ table }: Props): JSX.Element { function EntityList({ table }: Props): JSX.Element
const tableNameParam = useParams().tableName; {
const tableName = table === null ? tableNameParam : table.name; const tableNameParam = useParams().tableName;
const tableName = table === null ? tableNameParam : table.name;
const [tableState, setTableState] = useState(""); const [tableState, setTableState] = useState("");
const [filtersMenu, setFiltersMenu] = useState(null); const [filtersMenu, setFiltersMenu] = useState(null);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10); const [rowsPerPage, setRowsPerPage] = useState(10);
const [selectedIds, setSelectedIds] = useState([] as string[]); const [selectedIds, setSelectedIds] = useState([] as string[]);
const [columns, setColumns] = useState([] as GridColDef[]); const [columns, setColumns] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]); const [rows, setRows] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [columnVisibilityModel, setColumnVisibilityModel] = useState({}); const [columnVisibilityModel, setColumnVisibilityModel] = useState({});
const [sortModel, setSortModel] = useState([] as GridSortItem[]); const [sortModel, setSortModel] = useState([] as GridSortItem[]);
const [filterModel, setFilterModel] = useState(null as GridFilterModel); const [filterModel, setFilterModel] = useState(null as GridFilterModel);
const [alertContent, setAlertContent] = useState(""); const [alertContent, setAlertContent] = useState("");
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
const openFiltersMenu = (event: any) => setFiltersMenu(event.currentTarget); const openFiltersMenu = (event: any) => setFiltersMenu(event.currentTarget);
const closeFiltersMenu = () => setFiltersMenu(null); const closeFiltersMenu = () => setFiltersMenu(null);
const translateCriteriaOperator = (operator: string) => { const translateCriteriaOperator = (operator: string) =>
switch (operator) { {
switch (operator)
{
case "contains": case "contains":
return QCriteriaOperator.CONTAINS; return QCriteriaOperator.CONTAINS;
case "starts with": case "starts with":
return QCriteriaOperator.STARTS_WITH; return QCriteriaOperator.STARTS_WITH;
case "ends with": case "ends with":
return QCriteriaOperator.STARTS_WITH; return QCriteriaOperator.STARTS_WITH;
case "is": case "is":
case "equals": case "equals":
case "=": case "=":
return QCriteriaOperator.EQUALS; return QCriteriaOperator.EQUALS;
case "is not": case "is not":
case "!=": case "!=":
return QCriteriaOperator.NOT_EQUALS; return QCriteriaOperator.NOT_EQUALS;
case "is after": case "is after":
case ">": case ">":
return QCriteriaOperator.GREATER_THAN; return QCriteriaOperator.GREATER_THAN;
case "is on or after": case "is on or after":
case ">=": case ">=":
return QCriteriaOperator.GREATER_THAN_OR_EQUALS; return QCriteriaOperator.GREATER_THAN_OR_EQUALS;
case "is before": case "is before":
case "<": case "<":
return QCriteriaOperator.LESS_THAN; return QCriteriaOperator.LESS_THAN;
case "is on or before": case "is on or before":
case "<=": case "<=":
return QCriteriaOperator.LESS_THAN_OR_EQUALS; return QCriteriaOperator.LESS_THAN_OR_EQUALS;
case "is empty": case "is empty":
return QCriteriaOperator.IS_BLANK; return QCriteriaOperator.IS_BLANK;
case "is not empty": case "is not empty":
return QCriteriaOperator.IS_NOT_BLANK; return QCriteriaOperator.IS_NOT_BLANK;
// case "is any of": // case "is any of":
// TODO: handle this case // TODO: handle this case
default: default:
return QCriteriaOperator.EQUALS; return QCriteriaOperator.EQUALS;
} }
}; };
const buildQFilter = () => { const buildQFilter = () =>
const qFilter = new QQueryFilter(); {
sortModel.forEach((gridSortItem) => { const qFilter = new QQueryFilter();
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc")); sortModel.forEach((gridSortItem) =>
}); {
if (filterModel) { qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
filterModel.items.forEach((item) => {
qFilter.addCriteria(
new QFilterCriteria(item.columnField, translateCriteriaOperator(item.operatorValue), [
item.value,
])
);
}); });
} if (filterModel)
{
return qFilter; filterModel.items.forEach((item) =>
}; {
qFilter.addCriteria(
const updateTable = () => { new QFilterCriteria(item.columnField, translateCriteriaOperator(item.operatorValue), [
(async () => { item.value,
const tableMetaData = await QClient.loadTableMetaData(tableName); ]),
const count = await QClient.count(tableName); );
setTotalRecords(count); });
if (sortModel.length === 0) {
sortModel.push({ field: tableMetaData.primaryKeyField, sort: "desc" });
setSortModel(sortModel);
} }
const qFilter = buildQFilter(); return qFilter;
const columns = [] as GridColDef[]; };
const results = await QClient.query( const updateTable = () =>
tableName, {
qFilter, (async () =>
rowsPerPage, {
pageNumber * rowsPerPage const tableMetaData = await QClient.loadTableMetaData(tableName);
).catch((error) => { const count = await QClient.count(tableName);
if (error.message) { setTotalRecords(count);
setAlertContent(error.message);
} else {
setAlertContent(error.response.data.error);
}
throw error;
});
const rows = [] as any[]; if (sortModel.length === 0)
results.forEach((record) => { {
rows.push(Object.fromEntries(record.values.entries())); sortModel.push({ field: tableMetaData.primaryKeyField, sort: "desc" });
}); setSortModel(sortModel);
}
const sortedKeys = [...tableMetaData.fields.keys()].sort(); const qFilter = buildQFilter();
sortedKeys.forEach((key) => { const columns = [] as GridColDef[];
const field = tableMetaData.fields.get(key);
let columnType = "string"; const results = await QClient.query(
switch (field.type) { tableName,
case QFieldType.DECIMAL: qFilter,
case QFieldType.INTEGER: rowsPerPage,
columnType = "number"; pageNumber * rowsPerPage,
break; ).catch((error) =>
case QFieldType.DATE: {
columnType = "date"; if (error.message)
break; {
case QFieldType.DATE_TIME: setAlertContent(error.message);
columnType = "dateTime"; }
break; else
case QFieldType.BOOLEAN: {
columnType = "boolean"; setAlertContent(error.response.data.error);
break; }
default: throw error;
});
const rows = [] as any[];
results.forEach((record) =>
{
rows.push(Object.fromEntries(record.values.entries()));
});
const sortedKeys = [...tableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) =>
{
const field = tableMetaData.fields.get(key);
let columnType = "string";
switch (field.type)
{
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
columnType = "number";
break;
case QFieldType.DATE:
columnType = "date";
break;
case QFieldType.DATE_TIME:
columnType = "dateTime";
break;
case QFieldType.BOOLEAN:
columnType = "boolean";
break;
default:
// noop // noop
} }
const column = { const column = {
field: field.name, field: field.name,
type: columnType, type: columnType,
headerName: field.label, headerName: field.label,
width: 200, width: 200,
}; };
if (key === tableMetaData.primaryKeyField) { if (key === tableMetaData.primaryKeyField)
column.width = 75; {
columns.splice(0, 0, column); column.width = 75;
} else { columns.splice(0, 0, column);
columns.push(column); }
} else
{
columns.push(column);
}
});
const columnVisibilityModel = localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY);
if (columnVisibilityModel)
{
setColumnVisibilityModel(
JSON.parse(localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY)),
);
}
setColumns(columns);
setRows(rows);
setLoading(false);
forceUpdate();
})();
};
const handlePageChange = (page: number) =>
{
setPageNumber(page);
};
const handleRowsPerPageChange = (size: number) =>
{
setRowsPerPage(size);
};
const handleRowClick = (params: GridRowParams) =>
{
document.location.href = `/${tableName}/${params.id}`;
};
const handleFilterChange = (filterModel: GridFilterModel) =>
{
setFilterModel(filterModel);
};
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) =>
{
const newSelectedIds: string[] = [];
selectionModel.forEach((value: GridRowId) =>
{
newSelectedIds.push(value as string);
}); });
setSelectedIds(newSelectedIds);
};
const columnVisibilityModel = localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY); const handleColumnVisibilityChange = (columnVisibilityModel: GridColumnVisibilityModel) =>
if (columnVisibilityModel) { {
setColumnVisibilityModel( setColumnVisibilityModel(columnVisibilityModel);
JSON.parse(localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY)) localStorage.setItem(
); COLUMN_VISIBILITY_LOCAL_STORAGE_KEY,
JSON.stringify(columnVisibilityModel),
);
};
const handleSortChange = (gridSort: GridSortModel) =>
{
setSortModel(gridSort);
localStorage.setItem(COLUMN_SORT_LOCAL_STORAGE_KEY, JSON.stringify(gridSort));
};
if (tableName !== tableState)
{
(async () =>
{
setTableState(tableName);
const metaData = await QClient.loadMetaData();
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
// reset rows to trigger rerender
setRows([]);
})();
}
function getRecordsQueryString()
{
if (selectedIds.length > 0)
{
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
} }
setColumns(columns); return "";
setRows(rows); }
setLoading(false);
forceUpdate();
})();
};
const handlePageChange = (page: number) => { const renderActionsMenu = (
setPageNumber(page); <Menu
}; anchorEl={actionsMenu}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
open={Boolean(actionsMenu)}
onClose={closeActionsMenu}
keepMounted
>
{tableProcesses.map((process) => (
<MenuItem key={process.name}>
<Link href={`/processes/${process.name}${getRecordsQueryString()}`}>{process.label}</Link>
</MenuItem>
))}
</Menu>
);
const handleRowsPerPageChange = (size: number) => { useEffect(() =>
setRowsPerPage(size); {
}; updateTable();
}, [pageNumber, rowsPerPage, tableState, sortModel, filterModel]);
const handleRowClick = (params: GridRowParams) => { return (
document.location.href = `/${tableName}/${params.id}`; <DashboardLayout>
}; <DashboardNavbar />
<MDBox my={3}>
const handleFilterChange = (filterModel: GridFilterModel) => { {alertContent ? (
setFilterModel(filterModel); <MDBox mb={3}>
}; <Alert severity="error">{alertContent}</Alert>
</MDBox>
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => { ) : (
const newSelectedIds: string[] = []; ""
selectionModel.forEach((value: GridRowId) => {
newSelectedIds.push(value as string);
});
setSelectedIds(newSelectedIds);
};
const handleColumnVisibilityChange = (columnVisibilityModel: GridColumnVisibilityModel) => {
setColumnVisibilityModel(columnVisibilityModel);
localStorage.setItem(
COLUMN_VISIBILITY_LOCAL_STORAGE_KEY,
JSON.stringify(columnVisibilityModel)
);
};
const handleSortChange = (gridSort: GridSortModel) => {
setSortModel(gridSort);
localStorage.setItem(COLUMN_SORT_LOCAL_STORAGE_KEY, JSON.stringify(gridSort));
};
if (tableName !== tableState) {
(async () => {
setTableState(tableName);
const metaData = await QClient.loadMetaData();
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
// reset rows to trigger rerender
setRows([]);
})();
}
function getRecordsQueryString() {
if (selectedIds.length > 0) {
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
}
return "";
}
const renderActionsMenu = (
<Menu
anchorEl={actionsMenu}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
open={Boolean(actionsMenu)}
onClose={closeActionsMenu}
keepMounted
>
{tableProcesses.map((process) => (
<MenuItem key={process.name}>
<Link href={`/processes/${process.name}${getRecordsQueryString()}`}>{process.label}</Link>
</MenuItem>
))}
</Menu>
);
useEffect(() => {
updateTable();
}, [pageNumber, rowsPerPage, tableState, sortModel, filterModel]);
return (
<DashboardLayout>
<DashboardNavbar />
<MDBox my={3}>
{alertContent ? (
<MDBox mb={3}>
<Alert severity="error">{alertContent}</Alert>
</MDBox>
) : (
""
)}
<MDBox display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}>
<Link href={`/${tableName}/create`}>
<MDButton variant="gradient" color="info">
new {tableName}
</MDButton>
</Link>
<MDBox display="flex">
{tableProcesses.length > 0 && (
<MDButton
variant={actionsMenu ? "contained" : "outlined"}
color="dark"
onClick={openActionsMenu}
>
actions&nbsp;
<Icon>keyboard_arrow_down</Icon>
</MDButton>
)} )}
{renderActionsMenu} <MDBox display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}>
</MDBox> <Link href={`/${tableName}/create`}>
</MDBox> <MDButton variant="gradient" color="info">
<Card> new
<MDBox height="100%"> {" "}
<DataGrid {tableName}
components={{ Toolbar: GridToolbar }} </MDButton>
paginationMode="server" </Link>
sortingMode="server"
filterMode="server" <MDBox display="flex">
page={pageNumber} {tableProcesses.length > 0 && (
checkboxSelection <MDButton
disableSelectionOnClick variant={actionsMenu ? "contained" : "outlined"}
autoHeight color="dark"
rows={rows} onClick={openActionsMenu}
columns={columns} >
rowBuffer={10} actions&nbsp;
rowCount={totalRecords} <Icon>keyboard_arrow_down</Icon>
pageSize={rowsPerPage} </MDButton>
rowsPerPageOptions={[10, 25, 50]} )}
onPageSizeChange={handleRowsPerPageChange} {renderActionsMenu}
onPageChange={handlePageChange} </MDBox>
onRowClick={handleRowClick} </MDBox>
density="compact" <Card>
loading={loading} <MDBox height="100%">
onFilterModelChange={handleFilterChange} <DataGrid
columnVisibilityModel={columnVisibilityModel} components={{ Toolbar: GridToolbar }}
onColumnVisibilityModelChange={handleColumnVisibilityChange} paginationMode="server"
onSelectionModelChange={selectionChanged} sortingMode="server"
onSortModelChange={handleSortChange} filterMode="server"
sortingOrder={["asc", "desc"]} page={pageNumber}
sortModel={sortModel} checkboxSelection
getRowClassName={(params) => disableSelectionOnClick
params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd" autoHeight
} rows={rows}
/> columns={columns}
</MDBox> rowBuffer={10}
</Card> rowCount={totalRecords}
</MDBox> pageSize={rowsPerPage}
<Footer /> rowsPerPageOptions={[10, 25, 50]}
</DashboardLayout> onPageSizeChange={handleRowsPerPageChange}
); onPageChange={handlePageChange}
onRowClick={handleRowClick}
density="compact"
loading={loading}
onFilterModelChange={handleFilterChange}
columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onSelectionModelChange={selectionChanged}
onSortModelChange={handleSortChange}
sortingOrder={["asc", "desc"]}
sortModel={sortModel}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
/>
</MDBox>
</Card>
</MDBox>
<Footer />
</DashboardLayout>
);
} }
EntityList.defaultProps = { EntityList.defaultProps = {
table: null, table: null,
}; };
export default EntityList; export default EntityList;

View File

@ -48,161 +48,186 @@ interface Props {
id: string; id: string;
} }
function ViewContents({ id }: Props): JSX.Element { function ViewContents({ id }: Props): JSX.Element
const { tableName } = useParams(); {
const { tableName } = useParams();
const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [nameValues, setNameValues] = useState([] as JSX.Element[]); const [nameValues, setNameValues] = useState([] as JSX.Element[]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
if (!asyncLoadInited) { if (!asyncLoadInited)
setAsyncLoadInited(true); {
setAsyncLoadInited(true);
(async () => { (async () =>
const tableMetaData = await qController.loadTableMetaData(tableName); {
setTableMetaData(tableMetaData); const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
const metaData = await qController.loadMetaData(); const metaData = await qController.loadMetaData();
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName));
const foundRecord = await qController.get(tableName, id); const foundRecord = await qController.get(tableName, id);
nameValues.push( nameValues.push(
<MDBox key={tableMetaData.primaryKeyField} display="flex" py={1} pr={2}> <MDBox key={tableMetaData.primaryKeyField} display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold" textTransform="capitalize"> <MDTypography variant="button" fontWeight="bold" textTransform="capitalize">
{tableMetaData.primaryKeyField}: &nbsp; {tableMetaData.primaryKeyField}
</MDTypography> : &nbsp;
<MDTypography variant="button" fontWeight="regular" color="text"> </MDTypography>
&nbsp;{id} <MDTypography variant="button" fontWeight="regular" color="text">
</MDTypography> &nbsp;
</MDBox> {id}
); </MDTypography>
</MDBox>,
);
const sortedKeys = [...foundRecord.values.keys()].sort(); const sortedKeys = [...foundRecord.values.keys()].sort();
sortedKeys.forEach((key) => { sortedKeys.forEach((key) =>
if (key !== tableMetaData.primaryKeyField) { {
nameValues.push( if (key !== tableMetaData.primaryKeyField)
<MDBox key={key} display="flex" py={1} pr={2}> {
<MDTypography variant="button" fontWeight="bold" textTransform="capitalize"> nameValues.push(
{tableMetaData.fields.get(key).label}: &nbsp; <MDBox key={key} display="flex" py={1} pr={2}>
</MDTypography> <MDTypography variant="button" fontWeight="bold" textTransform="capitalize">
<MDTypography variant="button" fontWeight="regular" color="text"> {tableMetaData.fields.get(key).label}
&nbsp;{foundRecord.values.get(key)} : &nbsp;
</MDTypography> </MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text">
&nbsp;
{foundRecord.values.get(key)}
</MDTypography>
</MDBox>,
);
}
});
setNameValues(nameValues);
forceUpdate();
})();
}
const handleClickConfirmOpen = () =>
{
setOpen(true);
};
const handleClickConfirmClose = () =>
{
setOpen(false);
};
const handleDelete = (event: { preventDefault: () => void }) =>
{
event.preventDefault();
(async () =>
{
await qController.delete(tableName, id).then(() =>
{
window.location.href = `/${tableName}`;
});
})();
};
const editPath = `/${tableName}/${id}/edit`;
const renderActionsMenu = (
<Menu
anchorEl={actionsMenu}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
open={Boolean(actionsMenu)}
onClose={closeActionsMenu}
keepMounted
>
{tableProcesses.map((process) => (
<MenuItem key={process.name}>
<Link href={`/processes/${process.name}?recordIds=${id}`}>{process.label}</Link>
</MenuItem>
))}
</Menu>
);
return (
<Card id="basic-info" sx={{ overflow: "visible" }}>
<MDBox p={3}>
<MDBox display="flex" justifyContent="space-between">
<MDTypography variant="h5">
Viewing
{" "}
{tableMetaData?.label}
{" "}
(
{id}
)
</MDTypography>
{tableProcesses.length > 0 && (
<MDButton
variant={actionsMenu ? "contained" : "outlined"}
color="dark"
onClick={openActionsMenu}
>
actions&nbsp;
<Icon>keyboard_arrow_down</Icon>
</MDButton>
)}
{renderActionsMenu}
</MDBox> </MDBox>
); </MDBox>
} <MDBox p={3}>{nameValues}</MDBox>
}); <MDBox component="form" pb={3} px={3}>
<Grid key="tres" container spacing={3}>
setNameValues(nameValues); <MDBox ml="auto" mr={3}>
forceUpdate(); <MDButton
})(); variant="gradient"
} color="primary"
size="small"
const handleClickConfirmOpen = () => { onClick={handleClickConfirmOpen}
setOpen(true); >
}; delete
{" "}
const handleClickConfirmClose = () => { {tableMetaData?.label}
setOpen(false); </MDButton>
}; <Dialog
open={open}
const handleDelete = (event: { preventDefault: () => void }) => { onClose={handleClickConfirmClose}
event.preventDefault(); aria-labelledby="alert-dialog-title"
(async () => { aria-describedby="alert-dialog-description"
await qController.delete(tableName, id).then(() => { >
window.location.href = `/${tableName}`; <DialogTitle id="alert-dialog-title">Confirm Deletion</DialogTitle>
}); <DialogContent>
})(); <DialogContentText id="alert-dialog-description">
}; Are you sure you want to delete this record?
</DialogContentText>
const editPath = `/${tableName}/${id}/edit`; </DialogContent>
<DialogActions>
const renderActionsMenu = ( <Button onClick={handleClickConfirmClose}>No</Button>
<Menu <Button onClick={handleDelete} autoFocus>
anchorEl={actionsMenu} Yes
anchorOrigin={{ vertical: "bottom", horizontal: "left" }} </Button>
transformOrigin={{ vertical: "top", horizontal: "left" }} </DialogActions>
open={Boolean(actionsMenu)} </Dialog>
onClose={closeActionsMenu} </MDBox>
keepMounted <MDBox>
> <MDButton variant="gradient" color="dark" size="small">
{tableProcesses.map((process) => ( <Link href={editPath}>
<MenuItem key={process.name}> edit
<Link href={`/processes/${process.name}?recordIds=${id}`}>{process.label}</Link> {tableMetaData?.label}
</MenuItem> </Link>
))} </MDButton>
</Menu> </MDBox>
); </Grid>
</MDBox>
return ( </Card>
<Card id="basic-info" sx={{ overflow: "visible" }}> );
<MDBox p={3}>
<MDBox display="flex" justifyContent="space-between">
<MDTypography variant="h5">
Viewing {tableMetaData?.label} ({id})
</MDTypography>
{tableProcesses.length > 0 && (
<MDButton
variant={actionsMenu ? "contained" : "outlined"}
color="dark"
onClick={openActionsMenu}
>
actions&nbsp;
<Icon>keyboard_arrow_down</Icon>
</MDButton>
)}
{renderActionsMenu}
</MDBox>
</MDBox>
<MDBox p={3}>{nameValues}</MDBox>
<MDBox component="form" pb={3} px={3}>
<Grid key="tres" container spacing={3}>
<MDBox ml="auto" mr={3}>
<MDButton
variant="gradient"
color="primary"
size="small"
onClick={handleClickConfirmOpen}
>
delete {tableMetaData?.label}
</MDButton>
<Dialog
open={open}
onClose={handleClickConfirmClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Confirm Deletion</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete this record?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClickConfirmClose}>No</Button>
<Button onClick={handleDelete} autoFocus>
Yes
</Button>
</DialogActions>
</Dialog>
</MDBox>
<MDBox>
<MDButton variant="gradient" color="dark" size="small">
<Link href={editPath}>edit {tableMetaData?.label}</Link>
</MDButton>
</MDBox>
</Grid>
</MDBox>
</Card>
);
} }
export default ViewContents; export default ViewContents;

View File

@ -26,26 +26,27 @@ import MDBox from "components/MDBox";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
import ViewContents from "./components/ViewContents"; import ViewContents from "./components/ViewContents";
function EntityView(): JSX.Element { function EntityView(): JSX.Element
const { id } = useParams(); {
const { id } = useParams();
return ( return (
<BaseLayout> <BaseLayout>
<MDBox mt={4}> <MDBox mt={4}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} lg={12}> <Grid item xs={12} lg={12}>
<MDBox mb={3}> <MDBox mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<ViewContents id={id} /> <ViewContents id={id} />
</Grid> </Grid>
</Grid> </Grid>
</MDBox> </MDBox>
</Grid> </Grid>
</Grid> </Grid>
</MDBox> </MDBox>
</BaseLayout> </BaseLayout>
); );
} }
export default EntityView; export default EntityView;

View File

@ -45,415 +45,491 @@ import { QJobStarted } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJ
import { QJobComplete } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete"; import { QJobComplete } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import { QJobError } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import { QJobError } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import { QJobRunning } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import { QJobRunning } from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
import { DataGrid, GridColDef, GridRowParams, GridRowsProp } from "@mui/x-data-grid"; import {
DataGrid, GridColDef, GridRowParams, GridRowsProp,
} from "@mui/x-data-grid";
import QDynamicForm from "../../components/QDynamicForm"; import QDynamicForm from "../../components/QDynamicForm";
import MDTypography from "../../../components/MDTypography"; import MDTypography from "../../../components/MDTypography";
function getDynamicStepContent( function getDynamicStepContent(
stepIndex: number, stepIndex: number,
step: any, step: any,
formData: any, formData: any,
processError: string, processError: string,
processValues: any, processValues: any,
recordConfig: any recordConfig: any,
): JSX.Element { ): JSX.Element
const { formFields, values, errors, touched } = formData; {
// console.log(`in getDynamicStepContent: step label ${step?.label}`); const {
formFields, values, errors, touched,
} = formData;
// console.log(`in getDynamicStepContent: step label ${step?.label}`);
if (processError) { if (processError)
return ( {
return (
<>
<MDTypography color="error" variant="h3">
Error
</MDTypography>
<div>{processError}</div>
</>
);
}
if (!Object.keys(formFields).length)
{
// console.log("in getDynamicStepContent. No fields yet, so returning 'loading'");
return <div>Loading...</div>;
}
console.log(`in getDynamicStepContent. the step looks like: ${JSON.stringify(step)}`);
return (
<> <>
<MDTypography color="error" variant="h3"> {step.formFields && <QDynamicForm formData={formData} formLabel={step.label} />}
Error {step.viewFields && (
</MDTypography> <div>
<div>{processError}</div> {step.viewFields.map((field: QFieldMetaData) => (
</> <div key={field.name}>
); <b>
} {field.label}
:
if (!Object.keys(formFields).length) { </b>
// console.log("in getDynamicStepContent. No fields yet, so returning 'loading'"); {" "}
return <div>Loading...</div>; {processValues[field.name]}
} </div>
))}
console.log(`in getDynamicStepContent. the step looks like: ${JSON.stringify(step)}`);
return (
<>
{step.formFields && <QDynamicForm formData={formData} formLabel={step.label} />}
{step.viewFields && (
<div>
{step.viewFields.map((field: QFieldMetaData) => (
<div key={field.name}>
<b>{field.label}:</b> {processValues[field.name]}
</div> </div>
))} )}
</div> {step.recordListFields && (
)} <div>
{step.recordListFields && ( <b>Records:</b>
<div> {" "}
<b>Records:</b> <br /> <br />
<MDBox height="100%"> <MDBox height="100%">
<DataGrid <DataGrid
page={recordConfig.pageNo} page={recordConfig.pageNo}
disableSelectionOnClick disableSelectionOnClick
autoHeight autoHeight
rows={recordConfig.rows} rows={recordConfig.rows}
columns={recordConfig.columns} columns={recordConfig.columns}
rowBuffer={10} rowBuffer={10}
rowCount={recordConfig.totalRecords} rowCount={recordConfig.totalRecords}
pageSize={recordConfig.rowsPerPage} pageSize={recordConfig.rowsPerPage}
rowsPerPageOptions={[10, 25, 50]} rowsPerPageOptions={[10, 25, 50]}
onPageSizeChange={recordConfig.handleRowsPerPageChange} onPageSizeChange={recordConfig.handleRowsPerPageChange}
onPageChange={recordConfig.handlePageChange} onPageChange={recordConfig.handlePageChange}
onRowClick={recordConfig.handleRowClick} onRowClick={recordConfig.handleRowClick}
paginationMode="server" paginationMode="server"
density="compact" density="compact"
loading={recordConfig.loading} loading={recordConfig.loading}
/> />
</MDBox> </MDBox>
</div> </div>
)} )}
</> </>
); );
} }
function trace(name: string, isComponent: boolean = false) { function trace(name: string, isComponent: boolean = false)
if (isComponent) { {
console.log(`COMPONENT: ${name}`); if (isComponent)
} else { {
console.log(` function: ${name}`); console.log(`COMPONENT: ${name}`);
} }
else
{
console.log(` function: ${name}`);
}
} }
const qController = new QController(""); const qController = new QController("");
function ProcessRun(): JSX.Element { function ProcessRun(): JSX.Element
const { processName } = useParams(); {
const [processUUID, setProcessUUID] = useState(null as string); const { processName } = useParams();
const [jobUUID, setJobUUID] = useState(null as string); const [processUUID, setProcessUUID] = useState(null as string);
const [activeStepIndex, setActiveStepIndex] = useState(0); const [jobUUID, setJobUUID] = useState(null as string);
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData); const [activeStepIndex, setActiveStepIndex] = useState(0);
const [newStep, setNewStep] = useState(null); const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]); const [newStep, setNewStep] = useState(null);
const [needInitialLoad, setNeedInitialLoad] = useState(true); const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
const [processMetaData, setProcessMetaData] = useState(null); const [needInitialLoad, setNeedInitialLoad] = useState(true);
const [processValues, setProcessValues] = useState({} as any); const [processMetaData, setProcessMetaData] = useState(null);
const [lastProcessResponse, setLastProcessResponse] = useState( const [processValues, setProcessValues] = useState({} as any);
null as QJobStarted | QJobComplete | QJobError | QJobRunning const [lastProcessResponse, setLastProcessResponse] = useState(
); null as QJobStarted | QJobComplete | QJobError | QJobRunning,
const [formId, setFormId] = useState(""); );
const [formFields, setFormFields] = useState({}); const [formId, setFormId] = useState("");
const [initialValues, setInitialValues] = useState({}); const [formFields, setFormFields] = useState({});
const [validations, setValidations] = useState({}); const [initialValues, setInitialValues] = useState({});
const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false); const [validations, setValidations] = useState({});
const [needRecords, setNeedRecords] = useState(false); const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false);
const [processError, setProcessError] = useState(null as string); const [needRecords, setNeedRecords] = useState(false);
const [recordConfig, setRecordConfig] = useState({} as any); const [processError, setProcessError] = useState(null as string);
const onLastStep = activeStepIndex === steps.length - 2; const [recordConfig, setRecordConfig] = useState({} as any);
const noMoreSteps = activeStepIndex === steps.length - 1; const onLastStep = activeStepIndex === steps.length - 2;
const noMoreSteps = activeStepIndex === steps.length - 1;
trace("ProcessRun", true); trace("ProcessRun", true);
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. // // handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() => { useEffect(() =>
trace("updateActiveStep"); {
trace("updateActiveStep");
if (!processMetaData) { if (!processMetaData)
console.log("No process meta data yet, so returning early"); {
return; console.log("No process meta data yet, so returning early");
} return;
// console.log(`Steps are: ${steps}`);
// console.log(`Setting step to ${newStep}`);
let newIndex = null;
if (typeof newStep === "number") {
newIndex = newStep as number;
} else if (typeof newStep === "string") {
for (let i = 0; i < steps.length; i++) {
if (steps[i].name === newStep) {
newIndex = i;
break;
}
}
}
if (newIndex === null) {
setProcessError(`Unknown process step ${newStep}.`);
}
setActiveStepIndex(newIndex);
if (steps) {
const activeStep = steps[newIndex];
setActiveStep(activeStep);
setFormId(activeStep.name);
///////////////////////////////////////////////////
// if this step has form fields, set up the form //
///////////////////////////////////////////////////
if (activeStep.formFields) {
const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(
activeStep.formFields
);
const initialValues: any = {};
activeStep.formFields.forEach((field) => {
initialValues[field.name] = processValues[field.name];
});
setFormFields(dynamicFormFields);
setInitialValues(initialValues);
setValidations(Yup.object().shape(formValidations));
} }
if (activeStep.recordListFields) { // console.log(`Steps are: ${steps}`);
const newRecordConfig = {} as any; // console.log(`Setting step to ${newStep}`);
newRecordConfig.pageNo = 1; let newIndex = null;
newRecordConfig.rowsPerPage = 20; if (typeof newStep === "number")
newRecordConfig.columns = [] as GridColDef[]; {
newRecordConfig.rows = []; newIndex = newStep as number;
newRecordConfig.totalRecords = 0;
newRecordConfig.handleRowsPerPageChange = null;
newRecordConfig.handlePageChange = null;
newRecordConfig.handleRowClick = null;
newRecordConfig.loading = true;
activeStep.recordListFields.forEach((field) => {
newRecordConfig.columns.push({ field: field.name, headerName: field.label });
});
setRecordConfig(newRecordConfig);
setNeedRecords(true);
} }
else if (typeof newStep === "string")
{
for (let i = 0; i < steps.length; i++)
{
if (steps[i].name === newStep)
{
newIndex = i;
break;
}
}
}
if (newIndex === null)
{
setProcessError(`Unknown process step ${newStep}.`);
}
setActiveStepIndex(newIndex);
if (steps)
{
const activeStep = steps[newIndex];
setActiveStep(activeStep);
setFormId(activeStep.name);
///////////////////////////////////////////////////
// if this step has form fields, set up the form //
///////////////////////////////////////////////////
if (activeStep.formFields)
{
const { dynamicFormFields, formValidations } = DynamicFormUtils.getFormData(
activeStep.formFields,
);
const initialValues: any = {};
activeStep.formFields.forEach((field) =>
{
initialValues[field.name] = processValues[field.name];
});
setFormFields(dynamicFormFields);
setInitialValues(initialValues);
setValidations(Yup.object().shape(formValidations));
}
if (activeStep.recordListFields)
{
const newRecordConfig = {} as any;
newRecordConfig.pageNo = 1;
newRecordConfig.rowsPerPage = 20;
newRecordConfig.columns = [] as GridColDef[];
newRecordConfig.rows = [];
newRecordConfig.totalRecords = 0;
newRecordConfig.handleRowsPerPageChange = null;
newRecordConfig.handlePageChange = null;
newRecordConfig.handleRowClick = null;
newRecordConfig.loading = true;
activeStep.recordListFields.forEach((field) =>
{
newRecordConfig.columns.push({ field: field.name, headerName: field.label });
});
setRecordConfig(newRecordConfig);
setNeedRecords(true);
}
// console.log(`in updateActiveStep: formFields ${JSON.stringify(dynamicFormFields)}`); // console.log(`in updateActiveStep: formFields ${JSON.stringify(dynamicFormFields)}`);
// console.log(`in updateActiveStep: initialValues ${JSON.stringify(initialValues)}`); // console.log(`in updateActiveStep: initialValues ${JSON.stringify(initialValues)}`);
}
}, [newStep]);
useEffect(() => {
if (needRecords) {
setNeedRecords(false);
(async () => {
const records = await qController.processRecords(
processName,
processUUID,
recordConfig.rowsPerPage * (recordConfig.pageNo - 1),
recordConfig.rowsPerPage
);
recordConfig.loading = false;
recordConfig.rows = [];
let rowId = 0;
records.forEach((record) => {
const row = Object.fromEntries(record.values.entries());
if (!row.id) {
row.id = ++rowId;
}
recordConfig.rows.push(row);
});
// todo count?
recordConfig.totalRecords = records.length;
setRecordConfig(recordConfig);
})();
}
}, [needRecords]);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() => {
if (lastProcessResponse) {
trace("handleProcessResponse");
setLastProcessResponse(null);
if (lastProcessResponse instanceof QJobComplete) {
const qJobComplete = lastProcessResponse as QJobComplete;
console.log("Setting new step.");
setNewStep(qJobComplete.nextStep);
setProcessValues(qJobComplete.values);
// console.log(`Updated process values: ${JSON.stringify(qJobComplete.values)}`);
} else if (lastProcessResponse instanceof QJobStarted) {
const qJobStarted = lastProcessResponse as QJobStarted;
setJobUUID(qJobStarted.jobUUID);
setNeedToCheckJobStatus(true);
} else if (lastProcessResponse instanceof QJobRunning) {
const qJobRunning = lastProcessResponse as QJobRunning;
setNeedToCheckJobStatus(true);
} else if (lastProcessResponse instanceof QJobError) {
const qJobError = lastProcessResponse as QJobError;
console.log(`Got an error from the backend... ${qJobError.error}`);
setProcessError(qJobError.error);
} }
} }, [newStep]);
}, [lastProcessResponse]);
///////////////////////////////////////////////////////////////////////// useEffect(() =>
// while a backend async job is running, periodically check its status // {
///////////////////////////////////////////////////////////////////////// if (needRecords)
useEffect(() => { {
if (needToCheckJobStatus) { setNeedRecords(false);
trace("checkJobStatus"); (async () =>
setNeedToCheckJobStatus(false); {
(async () => { const records = await qController.processRecords(
setTimeout(async () => { processName,
const processResponse = await qController.processJobStatus( processUUID,
processName, recordConfig.rowsPerPage * (recordConfig.pageNo - 1),
processUUID, recordConfig.rowsPerPage,
jobUUID );
); recordConfig.loading = false;
setLastProcessResponse(processResponse); recordConfig.rows = [];
}, 1500); let rowId = 0;
})(); records.forEach((record) =>
} {
}, [needToCheckJobStatus]); const row = Object.fromEntries(record.values.entries());
if (!row.id)
////////////////////////////////////////////////////////////////////////////////////////// {
// do the initial load of data for the process - that is, meta data, plus the init step // row.id = ++rowId;
////////////////////////////////////////////////////////////////////////////////////////// }
if (needInitialLoad) { recordConfig.rows.push(row);
trace("initialLoad"); });
setNeedInitialLoad(false); // todo count?
(async () => { recordConfig.totalRecords = records.length;
const { search } = useLocation(); setRecordConfig(recordConfig);
const urlSearchParams = new URLSearchParams(search); })();
let queryStringForInit = null;
if (urlSearchParams.get("recordIds")) {
queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get("recordIds")}`;
} else if (urlSearchParams.get("filterJSON")) {
queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get(
"filterJSON"
)}`;
} }
// todo once saved filters exist }, [needRecords]);
//else if(urlSearchParams.get("filterId")) {
// queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}`
// }
console.log(`@dk: Query String for init: ${queryStringForInit}`); //////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (lastProcessResponse)
{
trace("handleProcessResponse");
setLastProcessResponse(null);
if (lastProcessResponse instanceof QJobComplete)
{
const qJobComplete = lastProcessResponse as QJobComplete;
console.log("Setting new step.");
setNewStep(qJobComplete.nextStep);
setProcessValues(qJobComplete.values);
// console.log(`Updated process values: ${JSON.stringify(qJobComplete.values)}`);
}
else if (lastProcessResponse instanceof QJobStarted)
{
const qJobStarted = lastProcessResponse as QJobStarted;
setJobUUID(qJobStarted.jobUUID);
setNeedToCheckJobStatus(true);
}
else if (lastProcessResponse instanceof QJobRunning)
{
const qJobRunning = lastProcessResponse as QJobRunning;
setNeedToCheckJobStatus(true);
}
else if (lastProcessResponse instanceof QJobError)
{
const qJobError = lastProcessResponse as QJobError;
console.log(`Got an error from the backend... ${qJobError.error}`);
setProcessError(qJobError.error);
}
}
}, [lastProcessResponse]);
const processMetaData = await qController.loadProcessMetaData(processName); /////////////////////////////////////////////////////////////////////////
// console.log(processMetaData); // while a backend async job is running, periodically check its status //
setProcessMetaData(processMetaData); /////////////////////////////////////////////////////////////////////////
setSteps(processMetaData.frontendSteps); useEffect(() =>
{
if (needToCheckJobStatus)
{
trace("checkJobStatus");
setNeedToCheckJobStatus(false);
(async () =>
{
setTimeout(async () =>
{
const processResponse = await qController.processJobStatus(
processName,
processUUID,
jobUUID,
);
setLastProcessResponse(processResponse);
}, 1500);
})();
}
}, [needToCheckJobStatus]);
const processResponse = await qController.processInit(processName, queryStringForInit); //////////////////////////////////////////////////////////////////////////////////////////
setProcessUUID(processResponse.processUUID); // do the initial load of data for the process - that is, meta data, plus the init step //
setLastProcessResponse(processResponse); //////////////////////////////////////////////////////////////////////////////////////////
if (needInitialLoad)
{
trace("initialLoad");
setNeedInitialLoad(false);
(async () =>
{
const { search } = useLocation();
const urlSearchParams = new URLSearchParams(search);
let queryStringForInit = null;
if (urlSearchParams.get("recordIds"))
{
queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get(
"recordIds",
)}`;
}
else if (urlSearchParams.get("filterJSON"))
{
queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get(
"filterJSON",
)}`;
}
// todo once saved filters exist
//else if(urlSearchParams.get("filterId")) {
// queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}`
// }
console.log(`@dk: Query String for init: ${queryStringForInit}`);
const processMetaData = await qController.loadProcessMetaData(processName);
// console.log(processMetaData);
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
const processResponse = await qController.processInit(processName, queryStringForInit);
setProcessUUID(processResponse.processUUID);
setLastProcessResponse(processResponse);
// console.log(processResponse); // console.log(processResponse);
})(); })();
} }
////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
// handle the back button - todo - not really done at all // // handle the back button - todo - not really done at all //
// e.g., qqq needs to say when back is or isn't allowed, and we need to hit the backend upon backs. // // e.g., qqq needs to say when back is or isn't allowed, and we need to hit the backend upon backs. //
////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////
const handleBack = () => { const handleBack = () =>
trace("handleBack"); {
setNewStep(activeStepIndex - 1); trace("handleBack");
}; setNewStep(activeStepIndex - 1);
};
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
// handle user submitting the form - which in qqq means moving forward from any screen. // // handle user submitting the form - which in qqq means moving forward from any screen. //
////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any, actions: any) => { const handleSubmit = async (values: any, actions: any) =>
trace("handleSubmit"); {
trace("handleSubmit");
// todo - post? // todo - post?
let queryString = ""; let queryString = "";
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) =>
queryString += `${key}=${encodeURIComponent(values[key])}&`; {
}); queryString += `${key}=${encodeURIComponent(values[key])}&`;
});
actions.setSubmitting(false); actions.setSubmitting(false);
actions.resetForm(); actions.resetForm();
const processResponse = await qController.processStep( const processResponse = await qController.processStep(
processName, processName,
processUUID, processUUID,
activeStep.name, activeStep.name,
queryString queryString,
); );
setLastProcessResponse(processResponse); setLastProcessResponse(processResponse);
}; };
return ( return (
<DashboardLayout> <DashboardLayout>
<DashboardNavbar /> <DashboardNavbar />
<MDBox py={3} mb={20} height="65vh"> <MDBox py={3} mb={20} height="65vh">
<Grid container justifyContent="center" alignItems="center" sx={{ height: "100%", mt: 8 }}> <Grid
<Grid item xs={12} lg={8}> container
<Formik justifyContent="center"
enableReinitialize alignItems="center"
initialValues={initialValues} sx={{ height: "100%", mt: 8 }}
validationSchema={validations}
onSubmit={handleSubmit}
> >
{({ values, errors, touched, isSubmitting }) => ( <Grid item xs={12} lg={8}>
<Form id={formId} autoComplete="off"> <Formik
<Card sx={{ height: "100%" }}> enableReinitialize
<MDBox mx={2} mt={-3}> initialValues={initialValues}
<Stepper activeStep={activeStepIndex} alternativeLabel> validationSchema={validations}
{steps.map((step) => ( onSubmit={handleSubmit}
<Step key={step.name}> >
<StepLabel>{step.label}</StepLabel> {({
</Step> values, errors, touched, isSubmitting,
))} }) => (
</Stepper> <Form id={formId} autoComplete="off">
</MDBox> <Card sx={{ height: "100%" }}>
<MDBox p={3}> <MDBox mx={2} mt={-3}>
<MDBox> <Stepper activeStep={activeStepIndex} alternativeLabel>
{/*************************************************************************** {steps.map((step) => (
** step content - e.g., the appropriate form or other screen for the step ** <Step key={step.name}>
***************************************************************************/} <StepLabel>{step.label}</StepLabel>
{getDynamicStepContent( </Step>
activeStepIndex, ))}
activeStep, </Stepper>
{ </MDBox>
values, <MDBox p={3}>
touched, <MDBox>
formFields, {/***************************************************************************
errors, ** step content - e.g., the appropriate form or other screen for the step **
}, ***************************************************************************/}
processError, {getDynamicStepContent(
processValues, activeStepIndex,
recordConfig activeStep,
)} {
{/******************************** values,
** back &| next/submit buttons ** touched,
********************************/} formFields,
<MDBox mt={2} width="100%" display="flex" justifyContent="space-between"> errors,
{true || activeStepIndex === 0 ? ( },
<MDBox /> processError,
) : ( processValues,
<MDButton variant="gradient" color="light" onClick={handleBack}> recordConfig,
back )}
</MDButton> {/********************************
)} ** back &| next/submit buttons **
{noMoreSteps || processError ? ( ********************************/}
<MDBox /> <MDBox
) : ( mt={2}
<MDButton width="100%"
disabled={isSubmitting} display="flex"
type="submit" justifyContent="space-between"
variant="gradient" >
color="dark" {true || activeStepIndex === 0 ? (
> <MDBox />
{onLastStep ? "submit" : "next"} ) : (
</MDButton> <MDButton
)} variant="gradient"
</MDBox> color="light"
</MDBox> onClick={handleBack}
</MDBox> >
</Card> back
</Form> </MDButton>
)} )}
</Formik> {noMoreSteps || processError ? (
</Grid> <MDBox />
</Grid> ) : (
</MDBox> <MDButton
<Footer /> disabled={isSubmitting}
</DashboardLayout> type="submit"
); variant="gradient"
color="dark"
>
{onLastStep ? "submit" : "next"}
</MDButton>
)}
</MDBox>
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
<Footer />
</DashboardLayout>
);
} }
export default ProcessRun; export default ProcessRun;

View File

@ -56,83 +56,85 @@ import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QContr
import EntityList from "./pages/entity-list"; import EntityList from "./pages/entity-list";
const qqqRoutes = [ const qqqRoutes = [
{ {
type: "collapse", type: "collapse",
name: "Brooklyn Alice", name: "Brooklyn Alice",
key: "brooklyn-alice", key: "brooklyn-alice",
icon: <MDAvatar src={profilePicture} alt="Brooklyn Alice" size="sm" />, icon: <MDAvatar src={profilePicture} alt="Brooklyn Alice" size="sm" />,
collapse: [ collapse: [
{ {
name: "My Profile", name: "My Profile",
key: "my-profile", key: "my-profile",
route: "/pages/profile/profile-overview", route: "/pages/profile/profile-overview",
component: <ProfileOverview />, component: <ProfileOverview />,
}, },
{ {
name: "Settings", name: "Settings",
key: "profile-settings", key: "profile-settings",
route: "/pages/account/settings", route: "/pages/account/settings",
component: <Settings />, component: <Settings />,
}, },
{ {
name: "Logout", name: "Logout",
key: "logout", key: "logout",
route: "/authentication/sign-in/basic", route: "/authentication/sign-in/basic",
component: <SignInBasic />, component: <SignInBasic />,
}, },
], ],
}, },
{ type: "divider", key: "divider-0" }, { type: "divider", key: "divider-0" },
{ {
type: "collapse", type: "collapse",
name: "Dashboards", name: "Dashboards",
key: "dashboards", key: "dashboards",
icon: <Icon fontSize="medium">dashboard</Icon>, icon: <Icon fontSize="medium">dashboard</Icon>,
collapse: [ collapse: [
{ {
name: "Analytics", name: "Analytics",
key: "analytics", key: "analytics",
route: "/dashboards/analytics", route: "/dashboards/analytics",
component: <Analytics />, component: <Analytics />,
}, },
{ {
name: "Sales", name: "Sales",
key: "sales", key: "sales",
route: "/dashboards/sales", route: "/dashboards/sales",
component: <Sales />, component: <Sales />,
}, },
], ],
}, },
{ type: "divider", key: "divider-1" }, { type: "divider", key: "divider-1" },
{ type: "title", title: "Tables", key: "title-docs" }, { type: "title", title: "Tables", key: "title-docs" },
]; ];
const qController = new QController(""); const qController = new QController("");
(async () => { (async () =>
const metaData = await qController.loadMetaData(); {
const metaData = await qController.loadMetaData();
// get the keys sorted // get the keys sorted
const keys = [...metaData.tables.keys()].sort(); const keys = [...metaData.tables.keys()].sort();
const tableList = [] as any[]; const tableList = [] as any[];
keys.forEach((key) => { keys.forEach((key) =>
const table = metaData.tables.get(key); {
tableList.push({ const table = metaData.tables.get(key);
name: `${table.label}`, tableList.push({
key: table.name, name: `${table.label}`,
route: `/${table.name}`, key: table.name,
component: <EntityList table={table} />, route: `/${table.name}`,
}); component: <EntityList table={table} />,
}); });
});
const tables = { const tables = {
type: "collapse", type: "collapse",
name: "Tables", name: "Tables",
key: "tables", key: "tables",
icon: <Icon fontSize="medium">dashboard</Icon>, icon: <Icon fontSize="medium">dashboard</Icon>,
collapse: tableList, collapse: tableList,
}; };
qqqRoutes.push(tables); qqqRoutes.push(tables);
})(); })();
export default qqqRoutes; export default qqqRoutes;

View File

@ -27,36 +27,44 @@ import { QQueryFilter } from "@kingsrook/qqq-frontend-core/lib/model/query/QQuer
** client wrapper of qqq backend ** client wrapper of qqq backend
** **
*******************************************************************************/ *******************************************************************************/
class QClient { class QClient
private static qController: QController; {
private static qController: QController;
private static getInstance() { private static getInstance()
if (this.qController == null) { {
this.qController = new QController(""); if (this.qController == null)
} {
this.qController = new QController("");
}
return this.qController; return this.qController;
} }
public static loadTableMetaData(tableName: string) { public static loadTableMetaData(tableName: string)
return this.getInstance().loadTableMetaData(tableName); {
} return this.getInstance().loadTableMetaData(tableName);
}
public static loadMetaData() { public static loadMetaData()
return this.getInstance().loadMetaData(); {
} return this.getInstance().loadMetaData();
}
public static query(tableName: string, filter: QQueryFilter, limit: number, skip: number) { public static query(tableName: string, filter: QQueryFilter, limit: number, skip: number)
return this.getInstance() {
.query(tableName, filter, limit, skip) return this.getInstance()
.catch((error) => { .query(tableName, filter, limit, skip)
throw error; .catch((error) =>
}); {
} throw error;
});
}
public static count(tableName: string) { public static count(tableName: string)
return this.getInstance().count(tableName); {
} return this.getInstance().count(tableName);
}
} }
export default QClient; export default QClient;

View File

@ -26,18 +26,22 @@ import { QInstance } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInst
** Utility class for working with QQQ Processes ** Utility class for working with QQQ Processes
** **
*******************************************************************************/ *******************************************************************************/
class QProcessUtils { class QProcessUtils
public static getProcessesForTable(metaData: QInstance, tableName: string): QProcessMetaData[] { {
const matchingProcesses: QProcessMetaData[] = []; public static getProcessesForTable(metaData: QInstance, tableName: string): QProcessMetaData[]
const processKeys = [...metaData.processes.keys()]; {
processKeys.forEach((key) => { const matchingProcesses: QProcessMetaData[] = [];
const process = metaData.processes.get(key); const processKeys = [...metaData.processes.keys()];
if (process.tableName === tableName) { processKeys.forEach((key) =>
matchingProcesses.push(process); {
} const process = metaData.processes.get(key);
}); if (process.tableName === tableName)
return matchingProcesses; {
} matchingProcesses.push(process);
}
});
return matchingProcesses;
}
} }
export default QProcessUtils; export default QProcessUtils;