Compare commits

..

65 Commits

Author SHA1 Message Date
894a9c2afc Merge branch 'release/0.22.0' 2024-09-04 20:15:14 -05:00
fd5055e502 Update versions for release 2024-09-04 20:00:10 -05:00
326367fbe0 Merge pull request #72 from Kingsrook/feature/CE-1647-update-filter-widget-to-show-preview
CE-1647: added preview to query filter widget
2024-09-04 16:17:40 -05:00
bb6f818457 Merge pull request #71 from Kingsrook/feature/CE-1646-possible-value-filter-bug
Feature/ce 1646 possible value filter bug
2024-09-04 16:17:25 -05:00
1cd6e07907 Merge pull request #70 from Kingsrook/feature/CE-1643-query-date-bugs
Feature/ce 1643 query date bugs
2024-09-04 16:16:59 -05:00
e839da6123 CE-1405 - Put margin-left on this, so if its used in a big number block, and it wraps, it does right, i think 2024-08-29 16:03:26 -05:00
34a4fc19b4 CE-1647: added preview to query filter widget 2024-08-27 17:52:34 -05:00
2cc7e9ebe1 CE-1643 Add a timezone conversion to the formatDate function for the case where it took a string rather than a Date as input, in which case, the new Date() call would be appying a timezone, and making us off-by-one (for some side of the prime merdian i think) 2024-08-23 15:13:44 -05:00
128a748b63 CE-1643 Add fontVariantNumeric: "tabular-nums" to the thing with numbers that count up, so it's awesome. 2024-08-23 15:11:52 -05:00
1284e3a22c CE-1643 change default operator for DATEs to be equals 2024-08-23 15:11:26 -05:00
ae358b9067 Merge tag 'version-0.21.0' into dev
Tag release
2024-08-23 14:42:25 -05:00
dc20c3d5ec Turn off postReleaseGoals=install in gitflow-maven-plugin 2024-08-23 14:41:55 -05:00
71a9c6470a Merge branch 'release/0.21.0' 2024-08-23 14:39:48 -05:00
765d40aef1 Add skipTestProject to gitflow-maven-plugin 2024-08-23 14:39:39 -05:00
d9f1642f0a Update for next development version 2024-08-23 14:10:54 -05:00
858540427d Update versions for release 2024-08-23 14:10:51 -05:00
eecb2d4489 Update qqq-backend-core dep to 0.21.0 2024-08-23 14:10:29 -05:00
5a6293cfdf Merged dev into feature/CE-1646-possible-value-filter-bug 2024-08-23 10:27:45 -05:00
868022408c Merge pull request #68 from Kingsrook/feature/CE-1555-ops-overview-fix-accordion
Feature/ce 1555 ops overview fix accordion
2024-08-23 10:26:53 -05:00
d090a665ff Merge pull request #69 from Kingsrook/feature/CE-1556-ops-overview-enhanced-tooltips
Feature/ce 1556 ops overview enhanced tooltips
2024-08-23 10:24:15 -05:00
f112cf5543 Remove sold border 2024-08-23 10:24:00 -05:00
0c2dcb1215 Update qqq-frontend-core to 1.0.105 2024-08-23 10:07:18 -05:00
418f7957a2 CE-1646 pass useCase (filter or form) into DynamicSelect and down to possibleValue backend calls 2024-08-23 08:51:24 -05:00
8be8bf367a CE-1405 / CE-1479 - Add missing ? 2024-08-21 08:52:57 -05:00
1ca1313a25 CE-1405 / CE-1479 - Let widget meta data default values set more grid cols per size classes 2024-08-21 08:35:35 -05:00
4533815535 CE-1554: added ability to overlay html over a block widget 2024-08-20 15:42:58 -05:00
4230f34b15 Only output Link if link has an href (else page blows up) 2024-08-20 10:07:45 -05:00
e08e37222b CE-1556: updated to try to use composite block data within tooltips 2024-08-13 16:22:12 -05:00
0ffada6aec CE-1555: updates 'accordian' behavior of tables 2024-08-12 12:09:29 -05:00
9f04d897a1 Merge branch 'feature/style-cleanups-20240725' into feature/CE-1555-ops-overview-fix-accordion 2024-08-09 13:44:52 -05:00
e604f47231 Fixes for data table css redo (a z-index on headers, and use background color (as sx prop) in body cells 2024-07-26 10:34:18 -05:00
93f5bb688c Merge pull request #66 from Kingsrook/feature/CE-1460-export-and-join-bugs
Feature/ce 1460 export and join bugs
2024-07-25 11:53:35 -05:00
3fa017e8b9 Update selector and widths per css change 2024-07-25 09:29:17 -05:00
9d5af539b9 Re-do css on table skeleton, per changes in the included DataTable*Cell components 2024-07-25 09:29:13 -05:00
97bab57974 Re-do css on tables, to do the whole table as divs with display: grid 2024-07-25 08:37:37 -05:00
d9de96ea7f Make whole top-right bar display:none at under md breakpoints 2024-07-25 08:36:13 -05:00
ff839d85fd Merged dev into feature/CE-1460-export-and-join-bugs 2024-07-09 11:36:32 -05:00
d31215f6c0 Added dev for build, not test 2024-07-09 10:09:25 -05:00
262855b9c0 Increase version to 0.21.0-SNAPSHOT 2024-07-09 10:08:32 -05:00
4d082c3c57 Update revision to 0.20.0, to publish that release version 2024-07-05 20:38:36 -05:00
45b6b42836 Add a ? in case no valueCount records came back - which, can happen for a join-field where there were no matching join records. 2024-07-05 12:42:42 -05:00
47fb7cc2e3 Merge pull request #65 from Kingsrook/feature/CE-1402-field-case-change-behaviors
Feature/ce 1402 field case change behaviors
2024-07-03 16:29:05 -05:00
647c63f5a3 Add ErrorBoundary, and wrap HelpContent with it 2024-06-25 13:32:13 -05:00
f545649882 CE-1402 Make consistent naming 'behaviors', not 'fieldBehaviors' 2024-06-25 08:40:30 -05:00
4d4610801f Add "&& npm dedupe --force" to clean-and-install 2024-06-25 07:58:29 -05:00
3ec43fbbd3 CE-1402 Only do flushSync and setSelectionRange after a toUpper/Lower and add a try-catch, just in case (specifically, because failed on input type=number) 2024-06-25 07:58:12 -05:00
28bc07cce4 CE-1402 Look for toUpperCase/toLowerCase behaviors on fields, and apply those transforms (plus cursor adjustments) in input fields and filter criteria values 2024-06-24 20:44:15 -05:00
c7d31fa39e Better matching for multi-word search terms ("one th" now matches "one two three") 2024-06-19 16:43:26 -05:00
69f1cfe92f Merge pull request #64 from Kingsrook/feature/fix-process-pvs-display-value
Fix to fetch possible-values when switching screens, to display label…
2024-06-19 16:32:38 -05:00
2ed95ff77a Update to not submit 'undefined' values to backend 2024-06-14 11:38:55 -05:00
66336a28ed Fix to fetch possible-values when switching screens, to display labels properly. 2024-06-14 09:11:15 -05:00
826bed4537 Add iconButton to open dot menu 2024-06-06 10:25:39 -05:00
40bd83cd96 attempt to fix scrollbar issue in 'is any of' mode 2024-06-05 14:06:18 -05:00
ca460e65e1 Merge pull request #63 from Kingsrook/feature/CE-938-order-release-automation
CE-938: fixed issue where modal record query was accepting shortcut k…
2024-06-05 10:49:23 -05:00
122fef152c CE-938: fixed issue where modal record query was accepting shortcut keys for new/copy/etc. 2024-06-05 10:38:30 -05:00
d0ed0ce949 Merge pull request #62 from Kingsrook/feature/CE-938-order-release-automation
Feature/ce 938 order release automation
2024-06-04 19:57:59 -05:00
b8aa36455d Merge pull request #61 from Kingsrook/feature/dot-menu-sort-filter-change
Feature/dot menu sort filter change
2024-06-04 19:56:48 -05:00
a778b7497a CE-938: updated to get filter and column setup values from widget data, rather than 'default values' 2024-06-04 13:43:39 -05:00
c3503a719f CE-938 - Add class & rule for margin of alert-widgets inside processes 2024-06-04 10:54:43 -05:00
2afa82c770 Fix 'Saved View' showing up in breadcrumb when it shouldn't 2024-06-04 10:54:43 -05:00
d03e908a9d CE-938: fixed bug on deletion of associated child records 2024-06-03 15:25:53 -05:00
dc62f97219 CE-938: updates from code review feedback 2024-06-03 11:28:29 -05:00
fe9e20715a CE-938: fixed bug where editing a record was not updating filter fields, fixed padding issue 2024-05-31 14:42:35 -05:00
7b562aea50 Slightly better sort for multi-word search terms 2024-05-17 17:11:35 -05:00
3bf1cea9dd Do custom sort & filter 2024-05-17 12:55:24 -05:00
39 changed files with 1324 additions and 730 deletions

View File

@ -115,7 +115,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ] context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters: filters:
branches: branches:
ignore: /(main|integration.*)/ ignore: /(main|dev|integration.*)/
tags: tags:
ignore: /(version|snapshot)-.*/ ignore: /(version|snapshot)-.*/
deploy: deploy:
@ -124,7 +124,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ] context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters: filters:
branches: branches:
only: /(main|integration.*)/ only: /(main|dev|integration.*)/
tags: tags:
only: /(version|snapshot)-.*/ only: /(version|snapshot)-.*/

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.102", "@kingsrook/qqq-frontend-core": "1.0.105",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",
@ -59,7 +59,7 @@
"build": "react-scripts build", "build": "react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib", "clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps", "clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps", "npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/", "prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start", "start": "BROWSER=none react-scripts --max-http-header-size=65535 start",

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<revision>0.20.0-SNAPSHOT</revision> <revision>0.22.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -66,7 +66,7 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.20.0-20240308.165846-65</version> <version>0.21.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
@ -154,11 +154,11 @@
<versionTagPrefix>version-</versionTagPrefix> <versionTagPrefix>version-</versionTagPrefix>
</gitFlowConfig> </gitFlowConfig>
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions --> <skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart> <commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor --> <versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
<versionProperty>revision</versionProperty> <versionProperty>revision</versionProperty>
<skipUpdateVersion>true</skipUpdateVersion> <skipUpdateVersion>true</skipUpdateVersion>
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
</configuration> </configuration>
</plugin> </plugin>

View File

@ -36,7 +36,7 @@ import Icon from "@mui/material/Icon";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {makeStyles} from "@mui/styles"; import {makeStyles} from "@mui/styles";
import {Command} from "cmdk"; import {Command} from "cmdk";
import React, {useContext, useEffect, useRef} from "react"; import React, {useContext, useEffect, useRef, useState} from "react";
import {useNavigate} from "react-router-dom"; import {useNavigate} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils"; import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils";
@ -62,16 +62,21 @@ const useStyles = makeStyles((theme: any) => ({
} }
})); }));
const A_FIRST = -1;
const B_FIRST = 1;
const CommandMenu = ({metaData}: Props) => const CommandMenu = ({metaData}: Props) =>
{ {
const [searchString, setSearchString] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
const pathParts = location.pathname.replace(/\/+$/, "").split("/"); const pathParts = location.pathname.replace(/\/+$/, "").split("/");
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses} = useContext(QContext); const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses, recordAnalytics} = useContext(QContext);
const classes = useStyles(); const classes = useStyles();
function evalueKeyPress(e: KeyboardEvent) function evaluateKeyPress(e: KeyboardEvent)
{ {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// if a dot pressed, not from a "text" element, then toggle command menu // // if a dot pressed, not from a "text" element, then toggle command menu //
@ -82,6 +87,7 @@ const CommandMenu = ({metaData}: Props) =>
if (e.key === "." && !keyboardHelpOpen) if (e.key === "." && !keyboardHelpOpen)
{ {
e.preventDefault(); e.preventDefault();
recordAnalytics({category: "globalEvents", action: "dotMenuKeyboardShortcut"});
setDotMenuOpen(true); setDotMenuOpen(true);
} }
else if (e.key === "?" && !dotMenuOpen) else if (e.key === "?" && !dotMenuOpen)
@ -107,20 +113,20 @@ const CommandMenu = ({metaData}: Props) =>
const down = (e: KeyboardEvent) => const down = (e: KeyboardEvent) =>
{ {
evalueKeyPress(e); evaluateKeyPress(e);
} };
document.addEventListener("keydown", down) document.addEventListener("keydown", down);
return () => return () =>
{ {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down);
} };
}, [tableMetaData, dotMenuOpen, keyboardHelpOpen]) }, [tableMetaData, dotMenuOpen, keyboardHelpOpen]);
useEffect(() => useEffect(() =>
{ {
setDotMenuOpen(false); setDotMenuOpen(false);
}, [location.pathname]) }, [location.pathname]);
function goToItem(path: string) function goToItem(path: string)
{ {
@ -162,73 +168,117 @@ const CommandMenu = ({metaData}: Props) =>
return (null); return (null);
} }
/*******************************************************************************
** sort a section (e.g, tables, apps).
**
** put labels that start-with the search word first.
*******************************************************************************/
function comparator(labelA: string, labelB: string)
{
if (searchString != "")
{
let aStartsWith = labelA.toLowerCase().startsWith(searchString.toLowerCase());
let bStartsWith = labelB.toLowerCase().startsWith(searchString.toLowerCase());
if (aStartsWith && !bStartsWith)
{
return A_FIRST;
}
else if (bStartsWith && !aStartsWith)
{
return B_FIRST;
}
const indexOfSpace = searchString.indexOf(" ");
if (indexOfSpace > 0)
{
aStartsWith = labelA.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase());
bStartsWith = labelB.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase());
if (aStartsWith && !bStartsWith)
{
return A_FIRST;
}
else if (bStartsWith && !aStartsWith)
{
return B_FIRST;
}
}
}
return (labelA.localeCompare(labelB));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function ActionsSection() function ActionsSection()
{ {
let tableNames : string[]= []; let tableNames: string[] = [];
metaData.tables.forEach((value: QTableMetaData, key: string) => metaData.tables.forEach((value: QTableMetaData, key: string) =>
{ {
tableNames.push(value.name); tableNames.push(value.name);
}) });
tableNames = tableNames.sort((a: string, b:string) => tableNames = tableNames.sort((a: string, b: string) =>
{ {
const labelA = metaData.tables.get(a).label ?? ""; const labelA = metaData.tables.get(a).label ?? "";
const labelB = metaData.tables.get(b).label ?? ""; const labelB = metaData.tables.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
const path = location.pathname; const path = location.pathname;
return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && ! path.endsWith("copy") && return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && !path.endsWith("copy") &&
( (
<Command.Group heading={`${tableMetaData.label} Actions`}> <Command.Group heading={`${tableMetaData.label} Actions`}>
{ {
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item>
} }
{ {
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission && tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item>
} }
{ {
tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission && tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item>
} }
{ {
metaData && metaData.tables.has("audit") && metaData && metaData.tables.has("audit") &&
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item>
} }
{ {
tableProcesses && tableProcesses.length > 0 && tableProcesses && tableProcesses.length > 0 &&
( (
tableProcesses.map((process) => ( tableProcesses.map((process) => (
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item> <Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item>
)) ))
) )
} }
<Command.Separator /> <Command.Separator />
</Command.Group> </Command.Group>
); );
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function TablesSection() function TablesSection()
{ {
let tableNames : string[]= []; let tableNames: string[] = [];
metaData.tables.forEach((value: QTableMetaData, key: string) => metaData.tables.forEach((value: QTableMetaData, key: string) =>
{ {
tableNames.push(value.name); tableNames.push(value.name);
}) });
tableNames = tableNames.sort((a: string, b:string) => tableNames = tableNames.sort((a: string, b: string) =>
{ {
const labelA = metaData.tables.get(a).label ?? ""; const labelA = metaData.tables.get(a).label ?? "";
const labelB = metaData.tables.get(b).label ?? ""; const labelB = metaData.tables.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
return( return (
<Command.Group heading="Tables"> <Command.Group heading="Tables">
{ {
tableNames.map((tableName: string, index: number) => tableNames.map((tableName: string, index: number) =>
@ -243,6 +293,7 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -252,16 +303,16 @@ const CommandMenu = ({metaData}: Props) =>
metaData.apps.forEach((value: QAppMetaData, key: string) => metaData.apps.forEach((value: QAppMetaData, key: string) =>
{ {
appNames.push(value.name); appNames.push(value.name);
}) });
appNames = appNames.sort((a: string, b:string) => appNames = appNames.sort((a: string, b: string) =>
{ {
const labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? ""; const labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? "";
const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? ""; const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
return( return (
<Command.Group heading="Apps"> <Command.Group heading="Apps">
{ {
appNames.map((appName: string, index: number) => appNames.map((appName: string, index: number) =>
@ -276,33 +327,37 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
/*******************************************************************************
**
*******************************************************************************/
function RecentlyViewedSection() function RecentlyViewedSection()
{ {
const history = HistoryUtils.get(); const history = HistoryUtils.get();
const options = [] as any; const options = [] as any;
history.entries.reverse().forEach((entry, index) => history.entries.reverse().forEach((entry, index) =>
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName}) options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
) );
let appNames: string[] = []; let appNames: string[] = [];
metaData.apps.forEach((value: QAppMetaData, key: string) => metaData.apps.forEach((value: QAppMetaData, key: string) =>
{ {
appNames.push(value.name); appNames.push(value.name);
}) });
appNames = appNames.sort((a: string, b:string) => appNames = appNames.sort((a: string, b: string) =>
{ {
const labelA = metaData.apps.get(a).label ?? ""; const labelA = metaData.apps.get(a).label ?? "";
const labelB = metaData.apps.get(b).label ?? ""; const labelB = metaData.apps.get(b).label ?? "";
return (labelA.localeCompare(labelB)); return comparator(labelA, labelB);
}) });
const entryMap = new Map<string, boolean>(); const entryMap = new Map<string, boolean>();
return( return (
<Command.Group heading="Recently Viewed Records"> <Command.Group heading="Recently Viewed Records">
{ {
history.entries.reverse().map((entry: QHistoryEntry, index: number) => history.entries.reverse().map((entry: QHistoryEntry, index: number) =>
! entryMap.has(entry.label) && entryMap.set(entry.label, true) && ( !entryMap.has(entry.label) && entryMap.set(entry.label, true) && (
<Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item> <Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item>
) )
) )
@ -311,29 +366,101 @@ const CommandMenu = ({metaData}: Props) =>
); );
} }
const containerElement = useRef(null) const containerElement = useRef(null);
/*******************************************************************************
**
*******************************************************************************/
function closeKeyboardHelp() function closeKeyboardHelp()
{ {
setKeyboardHelpOpen(false); setKeyboardHelpOpen(false);
} }
/*******************************************************************************
**
*******************************************************************************/
function closeDotMenu() function closeDotMenu()
{ {
setDotMenuOpen(false); setDotMenuOpen(false);
} }
/*******************************************************************************
** filter function for cmd-k library
**
*******************************************************************************/
function doFilter(value: string, search: string)
{
setSearchString(search);
/////////////////////
// split on spaces //
/////////////////////
const searchParts = search.toLowerCase().split(" ");
if (searchParts.length == 1)
{
//////////////////////////////////////////////
// if only 1 word, just do an includes test //
//////////////////////////////////////////////
return (value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0);
}
else
{
////////////////////////////////////////
// else split the value on spaces too //
////////////////////////////////////////
const valueParts = value.toLowerCase().split(" ");
if (searchParts.length > valueParts.length)
{
//////////////////////////////////////////////////////////////////////////////////
// if there are more words in the search than in the value, then it can't match //
// e.g. "order c" can't ever match, say "order" //
//////////////////////////////////////////////////////////////////////////////////
return (0);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// iterate over the search parts - if any don't match the corresponding value parts, then it's a non-match //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
let valueIndex = 0;
for (let i = 0; i < searchParts.length; i++)
{
let foundMatch = false;
for (; valueIndex < valueParts.length; valueIndex++)
{
if (valueParts[valueIndex].includes(searchParts[i]))
{
foundMatch = true;
break;
}
}
if (!foundMatch)
{
return (0);
}
}
/////////////////////////////////
// if no failure, return a hit //
/////////////////////////////////
return (1);
}
}
return ( return (
<React.Fragment> <React.Fragment>
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}> <Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
{ {
<Dialog open={dotMenuOpen} onClose={closeDotMenu}> <Dialog open={dotMenuOpen} onClose={closeDotMenu}>
<Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} label="Test Global Command Menu"> <Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} filter={(value, search) => doFilter(value, search)}>
<Box sx={{display: "flex"}}> <Box sx={{display: "flex"}}>
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..."/> <Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..." />
<Button onClick={closeDotMenu}><Icon>close</Icon></Button> <Button onClick={closeDotMenu}><Icon>close</Icon></Button>
</Box> </Box>
<Command.Loading /> <Command.Loading />
<Command.Separator /> <Command.Separator />
<Command.List> <Command.List>
<Command.Empty>No results found.</Command.Empty> <Command.Empty>No results found.</Command.Empty>
@ -381,6 +508,6 @@ const CommandMenu = ({metaData}: Props) =>
</Dialog> </Dialog>
} }
</React.Fragment> </React.Fragment>
) );
} };
export default CommandMenu; export default CommandMenu;

View File

@ -183,6 +183,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
bulkEditMode={bulkEditMode} bulkEditMode={bulkEditMode}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged} bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
otherValues={otherValuesMap} otherValues={otherValuesMap}
useCase="form"
/> />
{formattedHelpContent} {formattedHelpContent}
</Grid> </Grid>

View File

@ -19,16 +19,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {InputAdornment, InputLabel} from "@mui/material"; import {Box, InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik"; import {ErrorMessage, Field, useFormikContext} from "formik";
import React, {useState} from "react"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import React, {useMemo, useState} from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch"; import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
import MDInput from "qqq/components/legacy/MDInput"; import MDInput from "qqq/components/legacy/MDInput";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import {flushSync} from "react-dom";
// Declaring props types for FormField // Declaring props types for FormField
interface Props interface Props
@ -85,6 +86,51 @@ function QDynamicFormField({
} }
}; };
///////////////////////////////////////////////////////////////////////////////////////
// check the field meta data for behavior that says to do toUpperCase or toLowerCase //
///////////////////////////////////////////////////////////////////////////////////////
let isToUpperCase = useMemo(() => DynamicFormUtils.isToUpperCase(formFieldObject?.fieldMetaData), [formFieldObject]);
let isToLowerCase = useMemo(() => DynamicFormUtils.isToLowerCase(formFieldObject?.fieldMetaData), [formFieldObject]);
////////////////////////////////////////////////////////////////////////
// if the field has a toUpperCase or toLowerCase behavior on it, then //
// apply that rule. But also, to avoid the cursor always jumping to //
// the end of the input, do some manipulation of the selection. //
// See: https://giacomocerquone.com/blog/keep-input-cursor-still //
// Note, we only want an onChange handle if we're doing one of these //
// behaviors, (because teh flushSync is potentially slow). hence, we //
// put the onChange in an object and assign it with a spread //
////////////////////////////////////////////////////////////////////////
let onChange: any = {};
if (isToUpperCase || isToLowerCase)
{
onChange.onChange = (e: any) =>
{
const beforeStart = e.target.selectionStart;
const beforeEnd = e.target.selectionEnd;
flushSync(() =>
{
let newValue = e.currentTarget.value;
if (isToUpperCase)
{
newValue = newValue.toUpperCase();
}
if (isToLowerCase)
{
newValue = newValue.toLowerCase();
}
setFieldValue(name, newValue);
});
const input = document.getElementById(name) as HTMLInputElement;
if (input)
{
input.setSelectionRange(beforeStart, beforeEnd);
}
};
}
let field; let field;
let getsBulkEditHtmlLabel = true; let getsBulkEditHtmlLabel = true;
if (type === "checkbox") if (type === "checkbox")
@ -102,7 +148,7 @@ function QDynamicFormField({
else if (type === "ace") else if (type === "ace")
{ {
let mode = "text"; let mode = "text";
if(formFieldObject && formFieldObject.languageMode) if (formFieldObject && formFieldObject.languageMode)
{ {
mode = formFieldObject.languageMode; mode = formFieldObject.languageMode;
} }
@ -133,7 +179,7 @@ function QDynamicFormField({
{ {
field = ( field = (
<> <>
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled} <Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
onKeyPress={(e: any) => onKeyPress={(e: any) =>
{ {
if (e.key === "Enter") if (e.key === "Enter")
@ -173,7 +219,8 @@ function QDynamicFormField({
id={`bulkEditSwitch-${name}`} id={`bulkEditSwitch-${name}`}
checked={switchChecked} checked={switchChecked}
onClick={bulkEditSwitchChanged} onClick={bulkEditSwitchChanged}
sx={{top: "-4px", sx={{
top: "-4px",
"& .MuiSwitch-track": { "& .MuiSwitch-track": {
height: 20, height: 20,
borderRadius: 10, borderRadius: 10,

View File

@ -176,7 +176,7 @@ class DynamicFormUtils
initialDisplayValue: initialDisplayValue, initialDisplayValue: initialDisplayValue,
}; };
} }
else if(processName) else if (processName)
{ {
dynamicFormFields[field.name].possibleValueProps = dynamicFormFields[field.name].possibleValueProps =
{ {
@ -214,7 +214,7 @@ class DynamicFormUtils
if (Array.isArray(disabledFields)) if (Array.isArray(disabledFields))
{ {
return (disabledFields.indexOf(fieldName) > -1) return (disabledFields.indexOf(fieldName) > -1);
} }
else else
{ {
@ -222,6 +222,44 @@ class DynamicFormUtils
} }
} }
/***************************************************************************
* check if a field has the TO_UPPER_CASE behavior on it.
***************************************************************************/
public static isToUpperCase(fieldMetaData: QFieldMetaData): boolean
{
return this.hasBehavior(fieldMetaData, "TO_UPPER_CASE");
}
/***************************************************************************
* check if a field has the TO_LOWER_CASE behavior on it.
***************************************************************************/
public static isToLowerCase(fieldMetaData: QFieldMetaData): boolean
{
return this.hasBehavior(fieldMetaData, "TO_LOWER_CASE");
}
/***************************************************************************
* check if a field has a specific behavior name on it.
***************************************************************************/
private static hasBehavior(fieldMetaData: QFieldMetaData, behaviorName: string): boolean
{
if (fieldMetaData && fieldMetaData.behaviors)
{
for (let i = 0; i < fieldMetaData.behaviors.length; i++)
{
if (fieldMetaData.behaviors[i] == behaviorName)
{
return (true);
}
}
}
return (false);
}
} }
export default DynamicFormUtils; export default DynamicFormUtils;

View File

@ -53,6 +53,7 @@ interface Props
otherValues?: Map<string, any>; otherValues?: Map<string, any>;
variant: "standard" | "outlined"; variant: "standard" | "outlined";
initiallyOpen: boolean; initiallyOpen: boolean;
useCase: "form" | "filter";
} }
DynamicSelect.defaultProps = { DynamicSelect.defaultProps = {
@ -97,47 +98,47 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
borderColor: inputBorderColor borderColor: inputBorderColor
} }
}); });
} };
const qController = Client.getInstance(); const qController = Client.getInstance();
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props) function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
{ {
const [open, setOpen] = useState(initiallyOpen); const [open, setOpen] = useState(initiallyOpen);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]); const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null); const [searchTerm, setSearchTerm] = useState(null);
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues)))) const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))));
useEffect(() => useEffect(() =>
{ {
if(tableName && processName) if (tableName && processName)
{ {
console.log("DynamicSelect - you may not provide both a tableName and a processName") console.log("DynamicSelect - you may not provide both a tableName and a processName");
} }
if(tableName && !fieldName) if (tableName && !fieldName)
{ {
console.log("DynamicSelect - if you provide a tableName, you must also provide a fieldName"); console.log("DynamicSelect - if you provide a tableName, you must also provide a fieldName");
} }
if(processName && !fieldName) if (processName && !fieldName)
{ {
console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName"); console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName");
} }
if(!fieldName && !possibleValueSourceName) if (!fieldName && !possibleValueSourceName)
{ {
console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName"); console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName");
} }
if(fieldName && !possibleValueSourceName) if (fieldName && !possibleValueSourceName)
{ {
if(!tableName || !processName) if (!tableName || !processName)
{ {
console.log("DynamicSelect - if you provide a fieldName and not a possibleValueSourceName, then you must also provide a tableName or processName"); console.log("DynamicSelect - if you provide a fieldName and not a possibleValueSourceName, then you must also provide a tableName or processName");
} }
} }
if(possibleValueSourceName) if (possibleValueSourceName)
{ {
if(tableName || processName) if (tableName || processName)
{ {
console.log("DynamicSelect - if you provide a possibleValueSourceName, you should not also provide a tableName or processName"); console.log("DynamicSelect - if you provide a possibleValueSourceName, you should not also provide a tableName or processName");
} }
@ -173,7 +174,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
useEffect(() => useEffect(() =>
{ {
if(firstRender) if (firstRender)
{ {
// console.log("First render, so not searching..."); // console.log("First render, so not searching...");
setFirstRender(false); setFirstRender(false);
@ -194,9 +195,9 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
(async () => (async () =>
{ {
// console.log(`doing a search with ${searchTerm}`); // console.log(`doing a search with ${searchTerm}`);
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
if(tableMetaData == null && tableName) if (tableMetaData == null && tableName)
{ {
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName); let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData); setTableMetaData(tableMetaData);
@ -207,7 +208,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
// console.log(`${results}`); // console.log(`${results}`);
if (active) if (active)
{ {
setOptions([ ...results ]); setOptions([...results]);
} }
})(); })();
@ -215,30 +216,30 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
{ {
active = false; active = false;
}; };
}, [ searchTerm ]); }, [searchTerm]);
// todo - finish... call it in onOpen? // todo - finish... call it in onOpen?
const reloadIfOtherValuesAreChanged = () => const reloadIfOtherValuesAreChanged = () =>
{ {
if(JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded) if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
{ {
(async () => (async () =>
{ {
setLoading(true); setLoading(true);
setOptions([]); setOptions([]);
console.log("Refreshing possible values..."); console.log("Refreshing possible values...");
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues); const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
setLoading(false); setLoading(false);
setOptions([ ...results ]); setOptions([...results]);
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues))); setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
})(); })();
} }
} };
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) => const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
{ {
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`); // console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
if(reason !== "reset") if (reason !== "reset")
{ {
// console.log(` -> setting search term to ${value}`); // console.log(` -> setting search term to ${value}`);
setSearchTerm(value); setSearchTerm(value);
@ -248,7 +249,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
const handleBlur = (x: any) => const handleBlur = (x: any) =>
{ {
setSearchTerm(null); setSearchTerm(null);
} };
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) => const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
{ {
@ -256,9 +257,9 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
// console.log(value); // console.log(value);
setSearchTerm(null); setSearchTerm(null);
if(onChange) if (onChange)
{ {
if(isMultiple) if (isMultiple)
{ {
onChange(value); onChange(value);
} }
@ -267,7 +268,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
onChange(value ? new QPossibleValue(value) : null); onChange(value ? new QPossibleValue(value) : null);
} }
} }
else if(setFieldValueRef && fieldName) else if (setFieldValueRef && fieldName)
{ {
setFieldValueRef(fieldName, value ? value.id : null); setFieldValueRef(fieldName, value ? value.id : null);
} }
@ -280,7 +281,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
// get options whose text/label matches the input (e.g., not ids that match) // // get options whose text/label matches the input (e.g., not ids that match) //
///////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////
return (options); return (options);
} };
// @ts-ignore // @ts-ignore
const renderOption = (props: Object, option: any, {selected}) => const renderOption = (props: Object, option: any, {selected}) =>
@ -289,23 +290,24 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
try try
{ {
const field = tableMetaData?.fields.get(fieldName) const field = tableMetaData?.fields.get(fieldName);
if(field) if (field)
{ {
const adornment = field.getAdornment(AdornmentType.CHIP); const adornment = field.getAdornment(AdornmentType.CHIP);
if(adornment) if (adornment)
{ {
const color = adornment.getValue("color." + option.id) ?? "default" const color = adornment.getValue("color." + option.id) ?? "default";
const iconName = adornment.getValue("icon." + option.id) ?? null; const iconName = adornment.getValue("icon." + option.id) ?? null;
const iconElement = iconName ? <Icon>{iconName}</Icon> : null; const iconElement = iconName ? <Icon>{iconName}</Icon> : null;
content = (<Chip label={option.label} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />); content = (<Chip label={option.label} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />);
} }
} }
} }
catch(e) catch (e)
{ } {
}
if(isMultiple) if (isMultiple)
{ {
content = ( content = (
<> <>
@ -327,7 +329,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
{content} {content}
</li> </li>
); );
} };
const bulkEditSwitchChanged = () => const bulkEditSwitchChanged = () =>
{ {
@ -357,7 +359,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
{ {
setOpen(true); setOpen(true);
// console.log("setting open..."); // console.log("setting open...");
if(options.length == 0) if (options.length == 0)
{ {
// console.log("no options yet, so setting search term to ''..."); // console.log("no options yet, so setting search term to ''...");
setSearchTerm(""); setSearchTerm("");
@ -370,19 +372,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id} isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id}
getOptionLabel={(option) => getOptionLabel={(option) =>
{ {
if(option === null || option === undefined) if (option === null || option === undefined)
{ {
return (""); return ("");
} }
// @ts-ignore // @ts-ignore
if(option && option.length) if (option && option.length)
{ {
// @ts-ignore // @ts-ignore
option = option[0]; option = option[0];
} }
// @ts-ignore // @ts-ignore
return option.label return option.label;
}} }}
options={options} options={options}
loading={loading} loading={loading}
@ -446,7 +448,8 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
id={`bulkEditSwitch-${fieldName}`} id={`bulkEditSwitch-${fieldName}`}
checked={switchChecked} checked={switchChecked}
onClick={bulkEditSwitchChanged} onClick={bulkEditSwitchChanged}
sx={{top: "-4px", sx={{
top: "-4px",
"& .MuiSwitch-track": { "& .MuiSwitch-track": {
height: 20, height: 20,
borderRadius: 10, borderRadius: 10,
@ -465,7 +468,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
else else
{ {
return ( return (
<Box mb={1.5}> <Box>
{autocomplete} {autocomplete}
</Box> </Box>
); );

View File

@ -205,7 +205,7 @@ function EntityForm(props: Props): JSX.Element
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
const deleteChildRecord = (name: string, widgetData: any, rowIndex: number) => function deleteChildRecord(name: string, widgetData: any, rowIndex: number)
{ {
updateChildRecordList(name, "delete", rowIndex); updateChildRecordList(name, "delete", rowIndex);
}; };
@ -377,7 +377,7 @@ function EntityForm(props: Props): JSX.Element
widgetData.viewAllLink = null; widgetData.viewAllLink = null;
widgetMetaData.showExportButton = false; widgetMetaData.showExportButton = false;
return <RecordGridWidget return Object.keys(childListWidgetData).length > 0 && (<RecordGridWidget
key={`${formValues["tableName"]}-${modalDataChangedCounter}`} key={`${formValues["tableName"]}-${modalDataChangedCounter}`}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
data={widgetData} data={widgetData}
@ -387,7 +387,7 @@ function EntityForm(props: Props): JSX.Element
addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData)} addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData)}
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)} editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)}
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)} deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)}
/>; />);
} }
if (widgetMetaData.type == "filterAndColumnsSetup") if (widgetMetaData.type == "filterAndColumnsSetup")
@ -396,15 +396,16 @@ function EntityForm(props: Props): JSX.Element
// if the widget metadata specifies a table name, set form values to that so widget knows which to use // // if the widget metadata specifies a table name, set form values to that so widget knows which to use //
// (for the case when it is not being specified by a separate field in the record) // // (for the case when it is not being specified by a separate field in the record) //
///////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////
if (widgetMetaData?.defaultValues?.has("tableName")) if (widgetData?.tableName)
{ {
formValues["tableName"] = widgetMetaData?.defaultValues.get("tableName"); formValues["tableName"] = widgetData?.tableName;
} }
return <FilterAndColumnsSetupWidget return <FilterAndColumnsSetupWidget
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders... key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
isEditable={true} isEditable={true}
widgetMetaData={widgetMetaData} widgetMetaData={widgetMetaData}
widgetData={widgetData}
recordValues={formValues} recordValues={formValues}
onSaveCallback={setFormFieldValuesFromWidget} onSaveCallback={setFormFieldValuesFromWidget}
/>; />;
@ -480,83 +481,164 @@ function EntityForm(props: Props): JSX.Element
////////////////// //////////////////
// initial load // // initial load //
////////////////// //////////////////
if (!asyncLoadInited) useEffect(() =>
{ {
setAsyncLoadInited(true); if (!asyncLoadInited)
(async () =>
{ {
const tableMetaData = await qController.loadTableMetaData(tableName); setAsyncLoadInited(true);
setTableMetaData(tableMetaData); (async () =>
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
setupFieldRules(tableMetaData);
const metaData = await qController.loadMetaData();
setMetaData(metaData);
/////////////////////////////////////////////////
// define the sections, e.g., for the left-bar //
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{ {
const widget = metaData?.widgets.get(section.widgetName); const tableMetaData = await qController.loadTableMetaData(tableName);
if (widget) setTableMetaData(tableMetaData);
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
setupFieldRules(tableMetaData);
const metaData = await qController.loadMetaData();
setMetaData(metaData);
/////////////////////////////////////////////////
// define the sections, e.g., for the left-bar //
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{ {
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName")) const widget = metaData?.widgets.get(section.widgetName);
if (widget)
{ {
return (true); if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
{
return (true);
}
if (widget.type == "filterAndColumnsSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
{
return (true);
}
} }
if (widget.type == "filterAndColumnsSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm") return (false);
{ });
return (true); setTableSections(tableSections);
}
}
return (false); const fieldArray = [] as QFieldMetaData[];
}); const sortedKeys = [...tableMetaData.fields.keys()].sort();
setTableSections(tableSections); sortedKeys.forEach((key) =>
const fieldArray = [] as QFieldMetaData[];
const sortedKeys = [...tableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) =>
{
const fieldMetaData = tableMetaData.fields.get(key);
fieldArray.push(fieldMetaData);
});
/////////////////////////////////////////////////////////////////////////////////////////
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
/////////////////////////////////////////////////////////////////////////////////////////
let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (props.id !== null)
{
record = await qController.get(tableName, props.id);
setRecord(record);
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
const titleVerb = props.isCopy ? "Copy" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!props.isModal)
{ {
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`); const fieldMetaData = tableMetaData.fields.get(key);
} fieldArray.push(fieldMetaData);
tableMetaData.fields.forEach((fieldMetaData, key) =>
{
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
{
return;
}
initialValues[key] = record.values.get(key);
}); });
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block // // if doing an edit or copy, fetch the record and pre-populate the form values from it //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
if (!props.isCopy) let record: QRecord = null;
let defaultDisplayValues = new Map<string, string>();
if (props.id !== null)
{
record = await qController.get(tableName, props.id);
setRecord(record);
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
const titleVerb = props.isCopy ? "Copy" : "Edit";
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
if (!props.isModal)
{
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
}
tableMetaData.fields.forEach((fieldMetaData, key) =>
{
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
{
return;
}
initialValues[key] = record.values.get(key);
});
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (!props.isCopy)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
}
else
{
///////////////////////////////////////////
// else handle preparing to do an insert //
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`);
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
if (!props.isModal)
{
setPageHeader(`Creating New ${tableMetaData?.label}`);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. //
////////////////////////////////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name;
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue)
{
initialValues[fieldName] = defaultValue;
///////////////////////////////////////////////////////////////////////////////////////////
// we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed //
///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{
defaultDisplayValues.set(fieldName, results[0].label);
}
}
}
}
}
///////////////////////////////////////////////////
// if an override heading was passed in, use it. //
///////////////////////////////////////////////////
if (props.overrideHeading)
{
setFormTitle(props.overrideHeading);
if (!props.isModal)
{
setPageHeader(props.overrideHeading);
}
}
//////////////////////////////////////
// check capabilities & permissions //
//////////////////////////////////////
if (props.isCopy || !props.id)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
}
}
else
{ {
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{ {
@ -567,201 +649,123 @@ function EntityForm(props: Props): JSX.Element
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`); setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
} }
} }
}
else
{
///////////////////////////////////////////
// else handle preparing to do an insert //
///////////////////////////////////////////
setFormTitle(`Creating New ${tableMetaData?.label}`);
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
if (!props.isModal) /////////////////////////////////////////////////////////////////////
{ // make sure all initialValues are properly formatted for the form //
setPageHeader(`Creating New ${tableMetaData?.label}`); /////////////////////////////////////////////////////////////////////
}
////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. //
////////////////////////////////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++) for (let i = 0; i < fieldArray.length; i++)
{ {
const fieldMetaData = fieldArray[i]; const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name; if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue)
{ {
initialValues[fieldName] = defaultValue; initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
}
}
/////////////////////////////////////////////////////////////////////////////////////////// setInitialValues(initialValues);
// we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed // /////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////// // get formField and formValidation objects for Formik //
if (fieldMetaData.possibleValueSourceName) /////////////////////////////////////////////////////////
const {
dynamicFormFields,
formValidations,
} = DynamicFormUtils.getFormData(fieldArray, disabledFields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
/////////////////////////////////////
// group the formFields by section //
/////////////////////////////////////
const dynamicFormFieldsBySection = new Map<string, any>();
let t1sectionName;
let t1section;
const nonT1Sections: QTableSection[] = [];
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
const newChildListWidgetData: { [name: string]: ChildRecordListData } = {};
for (let i = 0; i < tableSections.length; i++)
{
const section = tableSections[i];
const sectionDynamicFormFields: any[] = [];
if (section.isHidden)
{
continue;
}
const hasFields = section.fieldNames && section.fieldNames.length > 0;
if (hasFields)
{
for (let j = 0; j < section.fieldNames.length; j++)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); const fieldName = section.fieldNames[j];
if (results && results.length > 0) const field = tableMetaData.fields.get(fieldName);
if (!field)
{ {
defaultDisplayValues.set(fieldName, results[0].label); console.log(`Omitting un-found field ${fieldName} from form`);
continue;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isCopy) || field.isEditable)
{
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
} }
} }
}
}
}
/////////////////////////////////////////////////// if (sectionDynamicFormFields.length === 0)
// if an override heading was passed in, use it. //
///////////////////////////////////////////////////
if (props.overrideHeading)
{
setFormTitle(props.overrideHeading);
if (!props.isModal)
{
setPageHeader(props.overrideHeading);
}
}
//////////////////////////////////////
// check capabilities & permissions //
//////////////////////////////////////
if (props.isCopy || !props.id)
{
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNotAllowedError("Records may not be created in this table");
}
else if (!tableMetaData.insertPermission)
{
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
}
}
else
{
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNotAllowedError("Records may not be edited in this table");
}
else if (!tableMetaData.editPermission)
{
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
}
}
/////////////////////////////////////////////////////////////////////
// make sure all initialValues are properly formatted for the form //
/////////////////////////////////////////////////////////////////////
for (let i = 0; i < fieldArray.length; i++)
{
const fieldMetaData = fieldArray[i];
if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
{
initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
}
}
setInitialValues(initialValues);
/////////////////////////////////////////////////////////
// get formField and formValidation objects for Formik //
/////////////////////////////////////////////////////////
const {
dynamicFormFields,
formValidations,
} = DynamicFormUtils.getFormData(fieldArray, disabledFields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
/////////////////////////////////////
// group the formFields by section //
/////////////////////////////////////
const dynamicFormFieldsBySection = new Map<string, any>();
let t1sectionName;
let t1section;
const nonT1Sections: QTableSection[] = [];
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
const newChildListWidgetData: { [name: string]: ChildRecordListData } = {};
for (let i = 0; i < tableSections.length; i++)
{
const section = tableSections[i];
const sectionDynamicFormFields: any[] = [];
if (section.isHidden)
{
continue;
}
const hasFields = section.fieldNames && section.fieldNames.length > 0;
if (hasFields)
{
for (let j = 0; j < section.fieldNames.length; j++)
{
const fieldName = section.fieldNames[j];
const field = tableMetaData.fields.get(fieldName);
if (!field)
{ {
console.log(`Omitting un-found field ${fieldName} from form`); ////////////////////////////////////////////////////////////////////////////////////////////////
// in case there are no active fields in this section, remove it from the tableSections array //
////////////////////////////////////////////////////////////////////////////////////////////////
tableSections.splice(i, 1);
i--;
continue; continue;
} }
else
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if ((props.id !== null && !props.isCopy) || field.isEditable)
{ {
sectionDynamicFormFields.push(dynamicFormFields[fieldName]); dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields);
} }
} }
if (sectionDynamicFormFields.length === 0)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// in case there are no active fields in this section, remove it from the tableSections array //
////////////////////////////////////////////////////////////////////////////////////////////////
tableSections.splice(i, 1);
i--;
continue;
}
else else
{ {
dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields); const widgetMetaData = metaData?.widgets.get(section.widgetName);
const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
newChildListWidgetData[section.widgetName] = widgetData;
}
//////////////////////////////////////
// capture the tier1 section's name //
//////////////////////////////////////
if (section.tier === "T1")
{
t1sectionName = section.name;
t1section = section;
}
else
{
nonT1Sections.push(section);
} }
} }
else
{
const widgetMetaData = metaData?.widgets.get(section.widgetName);
const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData); setT1SectionName(t1sectionName);
newChildListWidgetData[section.widgetName] = widgetData; setT1Section(t1section);
} setNonT1Sections(nonT1Sections);
setFormFields(dynamicFormFieldsBySection);
setValidations(Yup.object().shape(formValidations));
setRenderedWidgetSections(newRenderedWidgetSections);
setChildListWidgetData(newChildListWidgetData);
////////////////////////////////////// forceUpdate();
// capture the tier1 section's name // })();
////////////////////////////////////// }
if (section.tier === "T1") }, []);
{
t1sectionName = section.name;
t1section = section;
}
else
{
nonT1Sections.push(section);
}
}
setT1SectionName(t1sectionName);
setT1Section(t1section);
setNonT1Sections(nonT1Sections);
setFormFields(dynamicFormFieldsBySection);
setValidations(Yup.object().shape(formValidations));
setRenderedWidgetSections(newRenderedWidgetSections);
setChildListWidgetData(newChildListWidgetData);
forceUpdate();
})();
}
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -881,16 +885,28 @@ function EntityForm(props: Props): JSX.Element
let haveAssociationsToPost = false; let haveAssociationsToPost = false;
for (let name of Object.keys(childListWidgetData)) for (let name of Object.keys(childListWidgetData))
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if cannot find association name, continue loop, since cannot tell backend which association this is for //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName"); const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName");
if (!manageAssociationName) if (!manageAssociationName)
{ {
console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`); console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`);
continue;
} }
associationsToPost[manageAssociationName] = [];
haveAssociationsToPost = true; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (let i = 0; i < childListWidgetData[name].queryOutput?.records?.length; i++) // if the records array exists, add to associations to post - note: even if empty list, the backend will expect this //
// association name to be present if it is to act on it (for the case when all associations have been deleted) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (childListWidgetData[name].queryOutput.records)
{ {
associationsToPost[manageAssociationName].push(childListWidgetData[name].queryOutput.records[i].values); associationsToPost[manageAssociationName] = [];
haveAssociationsToPost = true;
for (let i = 0; i < childListWidgetData[name].queryOutput?.records?.length; i++)
{
associationsToPost[manageAssociationName].push(childListWidgetData[name].queryOutput.records[i].values);
}
} }
} }
if (haveAssociationsToPost) if (haveAssociationsToPost)

View File

@ -63,7 +63,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
// strip away empty elements of the route (e.g., trailing slash(es)) // // strip away empty elements of the route (e.g., trailing slash(es)) //
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
if(route.length) if (route.length)
{ {
// @ts-ignore // @ts-ignore
route = route.filter(r => r != ""); route = route.filter(r => r != "");
@ -74,18 +74,18 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
const fullPathToLabel = (fullPath: string, route: string): string => const fullPathToLabel = (fullPath: string, route: string): string =>
{ {
if(fullPath.endsWith("/")) if (fullPath.endsWith("/"))
{ {
fullPath = fullPath.replace(/\/+$/, ""); fullPath = fullPath.replace(/\/+$/, "");
} }
if(pathToLabelMap && pathToLabelMap[fullPath]) if (pathToLabelMap && pathToLabelMap[fullPath])
{ {
return pathToLabelMap[fullPath]; return pathToLabelMap[fullPath];
} }
return (routeToLabel(route)); return (routeToLabel(route));
} };
let pageTitle = branding?.appName ?? ""; let pageTitle = branding?.appName ?? "";
const fullRoutes: string[] = []; const fullRoutes: string[] = [];
@ -94,9 +94,9 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
{ {
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// avoid showing "saved view" as a breadcrumb element // // avoid showing "saved view" as a breadcrumb element //
// e.g., if at /app/table/savedView/1 (so where i==2) // // e.g., if at /app/table/savedView/1 //
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
if(routes[i] === "savedView" && i == 2) if (routes[i] === "savedView" && i == routes.length - 1)
{ {
continue; continue;
} }
@ -106,12 +106,12 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
// e.g., when at /app/table/savedView/1 (so where i==1) // // e.g., when at /app/table/savedView/1 (so where i==1) //
// we want to just be showing "App" // // we want to just be showing "App" //
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
if(i < routes.length - 1 && routes[i+1] == "savedView" && i == 1) if (i < routes.length - 1 && routes[i + 1] == "savedView" && i == 1)
{ {
continue; continue;
} }
if(routes[i] === "") if (routes[i] === "")
{ {
continue; continue;
} }

View File

@ -19,16 +19,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Popper, InputAdornment} from "@mui/material"; import {Popper, InputAdornment, Box} from "@mui/material";
import AppBar from "@mui/material/AppBar"; import AppBar from "@mui/material/AppBar";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import {Theme} from "@mui/material/styles";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import React, {useContext, useEffect, useState} from "react"; import React, {useContext, useEffect, useRef, useState} from "react";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs"; import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
@ -45,7 +45,8 @@ interface Props
isMini?: boolean; isMini?: boolean;
} }
interface HistoryEntry { interface HistoryEntry
{
id: number; id: number;
path: string; path: string;
label: string; label: string;
@ -64,7 +65,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const route = useLocation().pathname.split("/").slice(1); const route = useLocation().pathname.split("/").slice(1);
const navigate = useNavigate(); const navigate = useNavigate();
const {pageHeader} = useContext(QContext); const {pageHeader, setDotMenuOpen} = useContext(QContext);
useEffect(() => useEffect(() =>
{ {
@ -99,7 +100,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const options = [] as any; const options = [] as any;
history.entries.reverse().forEach((entry, index) => history.entries.reverse().forEach((entry, index) =>
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName}) options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
) );
setHistory(options); setHistory(options);
// Remove event listener on cleanup // Remove event listener on cleanup
@ -111,7 +112,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const goToHistory = (path: string) => const goToHistory = (path: string) =>
{ {
navigate(path); navigate(path);
} };
function buildHistoryEntries() function buildHistoryEntries()
{ {
@ -119,7 +120,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const options = [] as any; const options = [] as any;
history.entries.reverse().forEach((entry, index) => history.entries.reverse().forEach((entry, index) =>
options.push({label: entry.label, id: index, key: index, path: entry.path, iconName: entry.iconName}) options.push({label: entry.label, id: index, key: index, path: entry.path, iconName: entry.iconName})
) );
setHistory(options); setHistory(options);
} }
@ -133,12 +134,12 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const handleAutocompleteOnChange = (event: any, value: any, reason: any, details: any) => const handleAutocompleteOnChange = (event: any, value: any, reason: any, details: any) =>
{ {
if(value) if (value)
{ {
goToHistory(value.path); goToHistory(value.path);
} }
setAutocompleteValue(null); setAutocompleteValue(null);
} };
const CustomPopper = function (props: any) const CustomPopper = function (props: any)
{ {
@ -146,8 +147,8 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
{...props} {...props}
style={{whiteSpace: "nowrap", width: "auto"}} style={{whiteSpace: "nowrap", width: "auto"}}
placement="bottom-end" placement="bottom-end"
/>) />);
} };
const renderHistory = () => const renderHistory = () =>
{ {
@ -166,7 +167,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
PopperComponent={CustomPopper} PopperComponent={CustomPopper}
isOptionEqualToValue={(option, value) => option.id === value.id} isOptionEqualToValue={(option, value) => option.id === value.id}
sx={recentlyViewedMenu} sx={recentlyViewedMenu}
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{ renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
...params.InputProps, ...params.InputProps,
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
@ -184,7 +185,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
)} )}
/> />
); );
} };
// Styles for the navbar icons // Styles for the navbar icons
const iconsStyle = ({ const iconsStyle = ({
@ -210,21 +211,34 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const {pathToLabelMap} = useContext(QContext); const {pathToLabelMap} = useContext(QContext);
const fullPathToLabel = (fullPath: string, route: string): string => const fullPathToLabel = (fullPath: string, route: string): string =>
{ {
if(fullPath.endsWith("/")) if (fullPath.endsWith("/"))
{ {
fullPath = fullPath.replace(/\/+$/, ""); fullPath = fullPath.replace(/\/+$/, "");
} }
if(pathToLabelMap && pathToLabelMap[fullPath]) if (pathToLabelMap && pathToLabelMap[fullPath])
{ {
return pathToLabelMap[fullPath]; return pathToLabelMap[fullPath];
} }
return (routeToLabel(route)); return (routeToLabel(route));
} };
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]); const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
///////////////////////////////////////////////////////////////////////////////////////////////
// set the right-half of the navbar up so that below the 'md' breakpoint, it just disappears //
///////////////////////////////////////////////////////////////////////////////////////////////
const navbarRowRight = (theme: Theme, {isMini}: any) =>
{
return {
[theme.breakpoints.down("md")]: {
display: "none",
},
...navbarRow(theme, isMini)
}
};
return ( return (
<AppBar <AppBar
position={absolute ? "absolute" : navbarType} position={absolute ? "absolute" : navbarType}
@ -241,10 +255,15 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} /> <QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
</Box> </Box>
{isMini ? null : ( {isMini ? null : (
<Box sx={(theme) => navbarRow(theme, {isMini})}> <Box sx={(theme) => navbarRowRight(theme, {isMini})}>
<Box pr={0} mr={-2}> <Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
{renderHistory()} {renderHistory()}
</Box> </Box>
<Box mt={"-1rem"}>
<IconButton size="small" disableRipple color="inherit" onClick={() => setDotMenuOpen(true)}>
<Icon sx={iconsStyle} fontSize="small">search</Icon>
</IconButton>
</Box>
</Box> </Box>
)} )}
</Toolbar> </Toolbar>

View File

@ -0,0 +1,74 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import React, {Component, ErrorInfo} from "react";
interface Props
{
errorElement?: React.ReactNode;
children: React.ReactNode;
}
interface State
{
hasError: boolean;
}
/*******************************************************************************
** Component that you can wrap around other components that might throw an error,
** to give some isolation, rather than breaking a whole page.
** Credit: https://medium.com/@bobjunior542/how-to-use-error-boundaries-in-react-js-with-typescript-ee90ec814bf1
*******************************************************************************/
class ErrorBoundary extends Component<Props, State>
{
/***************************************************************************
*
***************************************************************************/
constructor(props: Props)
{
super(props);
this.state = {hasError: false};
}
/***************************************************************************
*
***************************************************************************/
componentDidCatch(error: Error, errorInfo: ErrorInfo)
{
console.error("ErrorBoundary caught an error: ", error, errorInfo);
this.setState({hasError: true});
}
/***************************************************************************
*
***************************************************************************/
render()
{
if (this.state.hasError)
{
return this.props.errorElement ?? <span>(Error)</span>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -22,6 +22,7 @@
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent"; import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import parse from "html-react-parser"; import parse from "html-react-parser";
import ErrorBoundary from "qqq/components/misc/ErrorBoundary";
import React, {useContext} from "react"; import React, {useContext} from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import QContext from "QContext"; import QContext from "QContext";
@ -128,6 +129,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles); let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
let content = null; let content = null;
let errorContent = "Error rendering help content.";
if (helpHelpActive) if (helpHelpActive)
{ {
if (!selectedHelpContent) if (!selectedHelpContent)
@ -135,6 +137,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
selectedHelpContent = new QHelpContent({content: ""}); selectedHelpContent = new QHelpContent({content: ""});
} }
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`; content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
errorContent += ` [${helpContentKey ?? "?"}]`;
} }
else if(selectedHelpContent) else if(selectedHelpContent)
{ {
@ -148,7 +151,9 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
{ {
return <Box display="inline" className="helpContent"> return <Box display="inline" className="helpContent">
{heading && <span className="header">{heading}</span>} {heading && <span className="header">{heading}</span>}
{formatHelpContent(content, selectedHelpContent.format)} <ErrorBoundary errorElement={<i>{errorContent}</i>}>
{formatHelpContent(content, selectedHelpContent.format)}
</ErrorBoundary>
</Box>; </Box>;
} }

View File

@ -79,6 +79,8 @@ interface BasicAndAdvancedQueryControlsProps
queryScreenUsage: QueryScreenUsage; queryScreenUsage: QueryScreenUsage;
allowVariables?: boolean;
mode: string; mode: string;
setMode: (mode: string) => void; setMode: (mode: string) => void;
} }
@ -181,7 +183,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
{ {
// todo - sometimes i want contains instead of equals on strings (client.name, for example...) // todo - sometimes i want contains instead of equals on strings (client.name, for example...)
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS; let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE) if (field?.type == QFieldType.DATE_TIME)
{ {
defaultOperator = QCriteriaOperator.GREATER_THAN; defaultOperator = QCriteriaOperator.GREATER_THAN;
} }
@ -676,6 +678,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
return (<QuickFilter return (<QuickFilter
key={fieldName} key={fieldName}
allowVariables={props.allowVariables}
fullFieldName={fieldName} fullFieldName={fieldName}
tableMetaData={tableMetaData} tableMetaData={tableMetaData}
updateCriteria={updateQuickCriteria} updateCriteria={updateQuickCriteria}
@ -701,6 +704,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
updateCriteria={updateQuickCriteria} updateCriteria={updateQuickCriteria}
criteriaParam={getQuickCriteriaParam(fieldName)} criteriaParam={getQuickCriteriaParam(fieldName)}
fieldMetaData={field} fieldMetaData={field}
allowVariables={props.allowVariables}
defaultOperator={defaultOperator} defaultOperator={defaultOperator}
queryScreenUsage={queryScreenUsage} queryScreenUsage={queryScreenUsage}
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />); handleRemoveQuickFilterField={handleRemoveQuickFilterField} />);

View File

@ -179,6 +179,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)} updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
removeCriteria={() => removeCriteria(index)} removeCriteria={() => removeCriteria(index)}
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)} updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
allowVariables={props.allowVariables}
queryScreenUsage={props.queryScreenUsage} queryScreenUsage={props.queryScreenUsage}
/> />
{/*JSON.stringify(criteria)*/} {/*JSON.stringify(criteria)*/}

View File

@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</>; return <span style={{fontVariantNumeric: "tabular-nums"}}>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</span>;
} }
const HOUR_MS = 60 * 60 * 1000; const HOUR_MS = 60 * 60 * 1000;

View File

@ -199,6 +199,7 @@ interface FilterCriteriaRowProps
removeCriteria: () => void; removeCriteria: () => void;
updateBooleanOperator: (newValue: string) => void; updateBooleanOperator: (newValue: string) => void;
queryScreenUsage?: QueryScreenUsage; queryScreenUsage?: QueryScreenUsage;
allowVariables?: boolean;
} }
FilterCriteriaRow.defaultProps = FilterCriteriaRow.defaultProps =
@ -267,7 +268,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
return {criteriaIsValid, criteriaStatusTooltip}; return {criteriaIsValid, criteriaStatusTooltip};
} }
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage}: FilterCriteriaRowProps): JSX.Element export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element
{ {
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`); // console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption); const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
@ -516,6 +517,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
table={fieldTable} table={fieldTable}
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)} valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
queryScreenUsage={queryScreenUsage} queryScreenUsage={queryScreenUsage}
allowVariables={allowVariables}
/> />
</Box> </Box>
<Box display="inline-block"> <Box display="inline-block">

View File

@ -30,6 +30,7 @@ import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect"; import DynamicSelect from "qqq/components/forms/DynamicSelect";
import AssignFilterVariable from "qqq/components/query/AssignFilterVariable"; import AssignFilterVariable from "qqq/components/query/AssignFilterVariable";
import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField"; import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField";
@ -39,7 +40,8 @@ import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
import React, {SyntheticEvent, useReducer, useState} from "react"; import React, {SyntheticEvent, useReducer} from "react";
import {flushSync} from "react-dom";
interface Props interface Props
{ {
@ -50,6 +52,7 @@ interface Props
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
initiallyOpenMultiValuePvs?: boolean; initiallyOpenMultiValuePvs?: boolean;
queryScreenUsage?: QueryScreenUsage; queryScreenUsage?: QueryScreenUsage;
allowVariables?: boolean;
} }
FilterCriteriaRowValues.defaultProps = FilterCriteriaRowValues.defaultProps =
@ -57,6 +60,10 @@ FilterCriteriaRowValues.defaultProps =
initiallyOpenMultiValuePvs: false initiallyOpenMultiValuePvs: false
}; };
/***************************************************************************
* get the type to use for an <input> from a QFieldMetaData
***************************************************************************/
export const getTypeForTextField = (field: QFieldMetaData): string => export const getTypeForTextField = (field: QFieldMetaData): string =>
{ {
let type = "search"; let type = "search";
@ -77,10 +84,15 @@ export const getTypeForTextField = (field: QFieldMetaData): string =>
return (type); return (type);
}; };
/***************************************************************************
* Make an <input type=text> (actually, might be a different type, but that's
* the gist of it), for a field.
***************************************************************************/
export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) => export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) =>
{ {
const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type;
const inputId = `${idPrefix}${criteria.id}`;
let type = getTypeForTextField(field); let type = getTypeForTextField(field);
const inputLabelProps: any = {}; const inputLabelProps: any = {};
@ -95,10 +107,13 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
value = ValueUtils.formatDateTimeValueForForm(value); value = ValueUtils.formatDateTimeValueForForm(value);
} }
/***************************************************************************
* Event handler for the clear 'x'.
***************************************************************************/
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) => const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
{ {
valueChangeHandler(event, index, ""); valueChangeHandler(event, index, "");
document.getElementById(`${idPrefix}${criteria.id}`).focus(); document.getElementById(inputId).focus();
}; };
@ -119,6 +134,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
}; };
/***************************************************************************
* make a version of the text field for when the criteria's value is set to
* be a "variable"
***************************************************************************/
const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") => const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
{ {
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) => const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
@ -148,6 +167,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
/></NoWrapTooltip>; /></NoWrapTooltip>;
}; };
///////////////////////////////////////////////////////////////////////////
// set up an 'x' icon as an end-adornment, to clear value from the field //
///////////////////////////////////////////////////////////////////////////
const inputProps: any = {}; const inputProps: any = {};
inputProps.endAdornment = ( inputProps.endAdornment = (
<InputAdornment position="end"> <InputAdornment position="end">
@ -157,18 +180,64 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
</InputAdornment> </InputAdornment>
); );
/***************************************************************************
* onChange event handler. deals with, if the field has a to upper/lower
* case rule on it, to apply that transform, and adjust the cursor.
* See: https://giacomocerquone.com/blog/keep-input-cursor-still
***************************************************************************/
function onChange(event: any)
{
const beforeStart = event.target.selectionStart;
const beforeEnd = event.target.selectionEnd;
let isToUpperCase = DynamicFormUtils.isToUpperCase(field);
let isToLowerCase = DynamicFormUtils.isToLowerCase(field);
if (isToUpperCase || isToLowerCase)
{
flushSync(() =>
{
let newValue = event.currentTarget.value;
if (isToUpperCase)
{
newValue = newValue.toUpperCase();
}
if (isToLowerCase)
{
newValue = newValue.toLowerCase();
}
event.currentTarget.value = newValue;
});
const input = document.getElementById(inputId);
if (input)
{
// @ts-ignore
input.setSelectionRange(beforeStart, beforeEnd);
}
}
valueChangeHandler(event, valueIndex);
}
////////////////////////
// return the element //
////////////////////////
return <Box sx={{margin: 0, padding: 0, display: "flex"}}> return <Box sx={{margin: 0, padding: 0, display: "flex"}}>
{ {
isExpression ? ( isExpression ? (
makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
) : ( ) : (
<TextField <TextField
id={`${idPrefix}${criteria.id}`} id={inputId}
label={label} label={label}
variant="standard" variant="standard"
autoComplete="off" autoComplete="off"
type={type} type={type}
onChange={(event) => valueChangeHandler(event, valueIndex)} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
value={value} value={value}
InputLabelProps={inputLabelProps} InputLabelProps={inputLabelProps}
@ -187,16 +256,23 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
}; };
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage}: Props): JSX.Element /***************************************************************************
* Component that is the "values" portion of a FilterCriteria Row in the
* advanced query filter editor.
***************************************************************************/
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage, allowVariables}: Props): JSX.Element
{ {
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [allowVariables, setAllowVariables] = useState(queryScreenUsage == "reportSetup");
if (!operatorOption) if (!operatorOption)
{ {
return null; return null;
} }
/***************************************************************************
* Callback for the Save button from the paste-values modal
***************************************************************************/
function saveNewPasterValues(newValues: any[]) function saveNewPasterValues(newValues: any[])
{ {
if (criteria.values) if (criteria.values)
@ -222,6 +298,9 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type; const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type;
//////////////////////////////////////////////////////////////////////////////
// render different form element9s) based on operator option's "value mode" //
//////////////////////////////////////////////////////////////////////////////
switch (operatorOption.valueMode) switch (operatorOption.valueMode)
{ {
case ValueMode.NONE: case ValueMode.NONE:
@ -298,6 +377,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
inForm={false} inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)} onChange={(value: any) => valueChangeHandler(null, 0, value)}
variant="standard" variant="standard"
useCase="filter"
/> />
</Box> </Box>
) )
@ -320,7 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
initialValues = criteria.values; initialValues = criteria.values;
} }
} }
return <Box mb={-1.5}> return <Box>
<DynamicSelect <DynamicSelect
tableName={table.name} tableName={table.name}
fieldName={field.name} fieldName={field.name}
@ -333,6 +413,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
inForm={false} inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)} onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard" variant="standard"
useCase="filter"
/> />
</Box>; </Box>;
} }

View File

@ -52,6 +52,7 @@ interface QuickFilterProps
defaultOperator?: QCriteriaOperator; defaultOperator?: QCriteriaOperator;
handleRemoveQuickFilterField?: (fieldName: string) => void; handleRemoveQuickFilterField?: (fieldName: string) => void;
queryScreenUsage?: QueryScreenUsage; queryScreenUsage?: QueryScreenUsage;
allowVariables?: boolean;
} }
QuickFilter.defaultProps = QuickFilter.defaultProps =
@ -141,7 +142,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
** Component to render a QuickFilter - that is - a button, with a Menu under it, ** Component to render a QuickFilter - that is - a button, with a Menu under it,
** with Operator and Value controls. ** with Operator and Value controls.
*******************************************************************************/ *******************************************************************************/
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage}: QuickFilterProps): JSX.Element export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage, allowVariables}: QuickFilterProps): JSX.Element
{ {
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : []; const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName); const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
@ -549,6 +550,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
criteria={criteria} criteria={criteria}
field={fieldMetaData} field={fieldMetaData}
table={tableForField} table={tableForField}
allowVariables={allowVariables}
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)} valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
initiallyOpenMultiValuePvs={true} // todo - maybe not? initiallyOpenMultiValuePvs={true} // todo - maybe not?
/> />

View File

@ -440,10 +440,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
<Box sx={{height: openTool ? "45%" : "100%"}}> <Box sx={{height: openTool ? "45%" : "100%"}}>
<Grid container alignItems="flex-end"> <Grid container alignItems="flex-end">
<Box maxWidth={"50%"} minWidth={300}> <Box maxWidth={"50%"} minWidth={300}>
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} /> <DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} useCase="form" />
</Box> </Box>
<Box maxWidth={"50%"} minWidth={300} pl={2}> <Box maxWidth={"50%"} minWidth={300} pl={2}>
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} /> <DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} useCase="form" />
</Box> </Box>
</Grid> </Grid>
<Box display="flex" sx={{height: "100%"}}> <Box display="flex" sx={{height: "100%"}}>

View File

@ -397,6 +397,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
initialDisplayValue={selectedAudienceOption?.label} initialDisplayValue={selectedAudienceOption?.label}
inForm={false} inForm={false}
onChange={handleAudienceChange} onChange={handleAudienceChange}
useCase="form"
/> />
</Box> </Box>
{/* {/*

View File

@ -22,16 +22,19 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Skeleton} from "@mui/material"; import {Box, Skeleton} from "@mui/material";
import parse from "html-react-parser";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import React from "react"; import React from "react";
interface CompositeData export interface CompositeData
{ {
blocks: BlockData[]; blocks: BlockData[];
styleOverrides?: any; styleOverrides?: any;
layout?: string; layout?: string;
overlayHtml?: string;
overlayStyleOverrides?: any;
} }
@ -97,20 +100,34 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
boxStyle.borderRadius = "0.5rem"; boxStyle.borderRadius = "0.5rem";
boxStyle.background = "#FFFFFF"; boxStyle.background = "#FFFFFF";
} }
if (data?.styleOverrides) if (data?.styleOverrides)
{ {
boxStyle = {...boxStyle, ...data.styleOverrides}; boxStyle = {...boxStyle, ...data.styleOverrides};
} }
return (<Box sx={boxStyle} className="compositeWidget"> let overlayStyle: any = {};
{
data.blocks.map((block: BlockData, index) => ( if (data?.overlayStyleOverrides)
<React.Fragment key={index}> {
<WidgetBlock widgetMetaData={widgetMetaData} block={block} /> overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
</React.Fragment> }
))
} return (
</Box>); <>
{
data?.overlayHtml &&
<Box sx={overlayStyle} className="blockWidgetOverlay">{parse(data.overlayHtml)}</Box>
}
<Box sx={boxStyle} className="compositeWidget">
{
data.blocks.map((block: BlockData, index) => (
<React.Fragment key={index}>
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
</React.Fragment>
))
}
</Box>
</>
);
} }

View File

@ -599,8 +599,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
} }
{ {
widgetMetaData.type === "filterAndColumnsSetup" && ( widgetMetaData.type === "filterAndColumnsSetup" && (
widgetData && widgetData[i] && widgetData[i].queryParams && widgetData && widgetData[i] &&
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() => <FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
{ {
}} /> }} />
) )
@ -638,8 +638,28 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
if (!omitWrappingGridContainer) if (!omitWrappingGridContainer)
{ {
// @ts-ignore const gridProps: {[key: string]: any} = {};
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
for(let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
{
const key = `gridCols:sizeClass:${size}`
if(widgetMetaData?.defaultValues?.has(key))
{
gridProps[size] = widgetMetaData?.defaultValues.get(key);
}
}
if(!gridProps["xxl"])
{
gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12;
}
if(!gridProps["xs"])
{
gridProps["xs"] = 12;
}
renderedWidget = (<Grid id={widgetMetaData.name} item {...gridProps} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
{renderedWidget} {renderedWidget}
</Grid>); </Grid>);
} }

View File

@ -21,18 +21,19 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Tooltip} from "@mui/material"; import {Box, Tooltip} from "@mui/material";
import React, {ReactElement, useContext} from "react";
import {Link} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent"; import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
import React, {ReactElement, useContext} from "react";
import {Link} from "react-router-dom";
interface BlockElementWrapperProps interface BlockElementWrapperProps
{ {
data: BlockData; data: BlockData;
metaData: QWidgetMetaData; metaData: QWidgetMetaData;
slot: string slot: string;
linkProps?: any; linkProps?: any;
children: ReactElement; children: ReactElement;
} }
@ -47,16 +48,16 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
let link: BlockLink; let link: BlockLink;
let tooltip: BlockTooltip; let tooltip: BlockTooltip;
if(slot) if (slot)
{ {
link = data.linkMap && data.linkMap[slot.toUpperCase()]; link = data.linkMap && data.linkMap[slot.toUpperCase()];
if(!link) if (!link)
{ {
link = data.link; link = data.link;
} }
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()]; tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
if(!tooltip) if (!tooltip)
{ {
tooltip = data.tooltip; tooltip = data.tooltip;
} }
@ -67,9 +68,9 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
tooltip = data.tooltip; tooltip = data.tooltip;
} }
if(!tooltip) if (!tooltip)
{ {
const helpRoles = ["ALL_SCREENS"] const helpRoles = ["ALL_SCREENS"];
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// the full keys in the helpContent table will look like: // // the full keys in the helpContent table will look like: //
@ -80,26 +81,39 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
const key = data.blockId ? `${data.blockId},${slot}` : slot; const key = data.blockId ? `${data.blockId},${slot}` : slot;
const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles); const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles);
if(showHelp) if (showHelp)
{ {
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={`widget:${metaData?.name};slot:${key}`} />; const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={`widget:${metaData?.name};slot:${key}`} />;
tooltip = {title: formattedHelpContent, placement: "bottom"} tooltip = {title: formattedHelpContent, placement: "bottom"};
} }
} }
let rs = children; let rs = children;
if(link) if (link && link.href)
{ {
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link> rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>;
} }
if(tooltip) if (tooltip)
{ {
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom" let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom";
// @ts-ignore - placement possible values // @ts-ignore - placement possible values
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip> if (tooltip.blockData)
{
// @ts-ignore - special case for composite type block...
rs = <Tooltip title={
<Box sx={{width: "200px"}}>
<CompositeWidget widgetMetaData={metaData} data={tooltip?.blockData} />
</Box>
}>{rs}</Tooltip>;
}
else
{
// @ts-ignore - placement possible values
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>;
}
} }
return (rs); return (rs);

View File

@ -20,6 +20,7 @@
*/ */
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
export interface BlockData export interface BlockData
@ -29,8 +30,8 @@ export interface BlockData
tooltip?: BlockTooltip; tooltip?: BlockTooltip;
link?: BlockLink; link?: BlockLink;
tooltipMap?: {[slot: string]: BlockTooltip}; tooltipMap?: { [slot: string]: BlockTooltip };
linkMap?: {[slot: string]: BlockLink}; linkMap?: { [slot: string]: BlockLink };
values: any; values: any;
styles?: any; styles?: any;
@ -39,6 +40,7 @@ export interface BlockData
export interface BlockTooltip export interface BlockTooltip
{ {
blockData?: CompositeData;
title: string | JSX.Element; title: string | JSX.Element;
placement: string; placement: string;
} }

View File

@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
return ( return (
<> <>
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}> <div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline", marginLeft: "auto"}}>
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}> <div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number"> <BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">

View File

@ -24,7 +24,6 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Collapse} from "@mui/material"; import {Alert, Collapse} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -49,6 +48,7 @@ interface FilterAndColumnsSetupWidgetProps
{ {
isEditable: boolean; isEditable: boolean;
widgetMetaData: QWidgetMetaData; widgetMetaData: QWidgetMetaData;
widgetData: any;
recordValues: { [name: string]: any }; recordValues: { [name: string]: any };
onSaveCallback?: (values: { [name: string]: any }) => void; onSaveCallback?: (values: { [name: string]: any }) => void;
} }
@ -83,10 +83,11 @@ const qController = Client.getInstance();
/******************************************************************************* /*******************************************************************************
** Component for editing the main setup of a report - that is: filter & columns ** Component for editing the main setup of a report - that is: filter & columns
*******************************************************************************/ *******************************************************************************/
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
{ {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [hideColumns, setHideColumns] = useState(widgetMetaData?.defaultValues?.has("hideColumns") && widgetMetaData?.defaultValues?.get("hideColumns")); const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [alertContent, setAlertContent] = useState(null as string); const [alertContent, setAlertContent] = useState(null as string);
@ -108,32 +109,38 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
let columns: QQueryColumns = null; let columns: QQueryColumns = null;
let usingDefaultEmptyFilter = false; let usingDefaultEmptyFilter = false;
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter; let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
if (!queryFilter) if (!queryFilter)
{ {
queryFilter = new QQueryFilter(); queryFilter = new QQueryFilter();
if (defaultFilterFields?.length == 0)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there is no queryFilter provided, see if there are default fields from which a query should be seeded //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
const defaultFilterFields = getDefaultFilterFieldNames(widgetMetaData);
if (defaultFilterFields?.length > 0)
{
defaultFilterFields.forEach((fieldName: string) =>
{
if (recordValues[fieldName])
{
queryFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [recordValues[fieldName]]));
}
});
queryFilter.addOrderBy(new QFilterOrderBy("id", false));
queryFilter = Object.assign({}, queryFilter);
}
else
{ {
usingDefaultEmptyFilter = true; usingDefaultEmptyFilter = true;
} }
} }
else
{
queryFilter = Object.assign(new QQueryFilter(), queryFilter);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if there are default fields from which a query should be seeded, add/update the filter with them //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if (defaultFilterFields?.length > 0)
{
defaultFilterFields.forEach((fieldName: string) =>
{
////////////////////////////////////////////////////////////////////////////////////////////
// if a value for the default field exists, remove the criteria for it in our query first //
////////////////////////////////////////////////////////////////////////////////////////////
queryFilter.criteria = queryFilter.criteria?.filter(c => c.fieldName != fieldName);
if (recordValues[fieldName])
{
queryFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [recordValues[fieldName]]));
}
});
}
if (recordValues["columnsJson"]) if (recordValues["columnsJson"])
{ {
@ -148,7 +155,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
//////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////
// if a default table name specified, use it, otherwise use it from the record values // // if a default table name specified, use it, otherwise use it from the record values //
//////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////
let tableName = widgetMetaData?.defaultValues?.get("tableName"); let tableName = widgetData?.tableName;
if (!tableName && recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"])) if (!tableName && recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
{ {
tableName = recordValues["tableName"]; tableName = recordValues["tableName"];
@ -169,27 +176,13 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
}, [JSON.stringify(recordValues)]); }, [JSON.stringify(recordValues)]);
/*******************************************************************************
**
*******************************************************************************/
function getDefaultFilterFieldNames(widgetMetaData: QWidgetMetaData)
{
if (widgetMetaData?.defaultValues?.has("filterDefaultFieldNames"))
{
return (widgetMetaData.defaultValues.get("filterDefaultFieldNames").split(","));
}
return ([]);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function openEditor() function openEditor()
{ {
let missingRequiredFields = [] as string[]; let missingRequiredFields = [] as string[];
getDefaultFilterFieldNames(widgetMetaData)?.forEach((fieldName: string) => widgetData?.filterDefaultFieldNames?.forEach((fieldName: string) =>
{ {
if (!recordValues[fieldName]) if (!recordValues[fieldName])
{ {
@ -202,7 +195,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
if (missingRequiredFields.length > 0) if (missingRequiredFields.length > 0)
{ {
setAlertContent("The following fields must first be selected to add Additional Order Filters: '" + missingRequiredFields.join(", ") + "'"); setAlertContent("The following fields must first be selected to edit the filter: '" + missingRequiredFields.join(", ") + "'");
return; return;
} }
@ -280,7 +273,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function mayShowQueryPreview(): boolean function mayShowQuery(): boolean
{ {
if (tableMetaData) if (tableMetaData)
{ {
@ -296,7 +289,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
function mayShowColumnsPreview(): boolean function mayShowColumns(): boolean
{ {
if (tableMetaData) if (tableMetaData)
{ {
@ -364,14 +357,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<Box pt="0.5rem"> <Box pt="0.5rem">
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<h5>Query Filter</h5> <h5>Query Filter</h5>
<Box fontSize="0.75rem" fontWeight="700">{mayShowQueryPreview() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box> <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
</Box> </Box>
{ {
mayShowQueryPreview() && mayShowQuery() &&
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} /> <AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
} }
{ {
!mayShowQueryPreview() && !mayShowQuery() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}> <Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
{ {
isEditable && isEditable &&
@ -390,11 +383,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<h5>Columns</h5> <h5>Columns</h5>
<Box display="flex" flexWrap="wrap" fontSize="1rem"> <Box display="flex" flexWrap="wrap" fontSize="1rem">
{ {
mayShowColumnsPreview() && mayShowColumns() && columns &&
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>) columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
} }
{ {
!mayShowColumnsPreview() && !mayShowColumns() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}> <Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
{ {
isEditable && isEditable &&
@ -410,6 +403,21 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
</Box> </Box>
</Box> </Box>
)} )}
{!hidePreview && !isEditable && frontendQueryFilter && tableMetaData && (
<Box pt="1rem">
<h5>Preview</h5>
<RecordQuery
allowVariables={widgetData?.allowVariables}
ref={recordQueryRef}
table={tableMetaData}
isPreview={true}
usage="reportSetup"
isModal={true}
initialQueryFilter={frontendQueryFilter}
initialColumns={columns}
/>
</Box>
)}
{ {
modalOpen && modalOpen &&
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}> <Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
@ -425,6 +433,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
} }
{ {
tableMetaData && <RecordQuery tableMetaData && <RecordQuery
allowVariables={widgetData?.allowVariables}
ref={recordQueryRef} ref={recordQueryRef}
table={tableMetaData} table={tableMetaData}
usage="reportSetup" usage="reportSetup"

View File

@ -40,14 +40,14 @@ import {Link, useNavigate} from "react-router-dom";
export interface ChildRecordListData extends WidgetData export interface ChildRecordListData extends WidgetData
{ {
title: string; title: string;
queryOutput: {records: {values: any}[]} queryOutput: { records: { values: any }[] };
childTableMetaData: QTableMetaData; childTableMetaData: QTableMetaData;
tablePath: string; tablePath: string;
viewAllLink: string; viewAllLink: string;
totalRows: number; totalRows: number;
canAddChildRecord: boolean; canAddChildRecord: boolean;
defaultValuesForNewChildRecords: {[fieldName: string]: any}; defaultValuesForNewChildRecords: { [fieldName: string]: any };
disabledFieldsForNewChildRecords: {[fieldName: string]: any}; disabledFieldsForNewChildRecords: { [fieldName: string]: any };
} }
interface Props interface Props
@ -75,9 +75,9 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
{ {
const instance = useRef({timer: null}); const instance = useRef({timer: null});
const [rows, setRows] = useState([]); const [rows, setRows] = useState([]);
const [records, setRecords] = useState([] as QRecord[]) const [records, setRecords] = useState([] as QRecord[]);
const [columns, setColumns] = useState([]); const [columns, setColumns] = useState([]);
const [allColumns, setAllColumns] = useState([]) const [allColumns, setAllColumns] = useState([]);
const [csv, setCsv] = useState(null as string); const [csv, setCsv] = useState(null as string);
const [fileName, setFileName] = useState(null as string); const [fileName, setFileName] = useState(null as string);
const [gridMouseDownX, setGridMouseDownX] = useState(0); const [gridMouseDownX, setGridMouseDownX] = useState(0);
@ -110,20 +110,20 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
// capture all-columns to use for the export (before we might splice some away from the on-screen display) // // capture all-columns to use for the export (before we might splice some away from the on-screen display) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
const allColumns = [... columns]; const allColumns = [...columns];
setAllColumns(JSON.parse(JSON.stringify(columns))); setAllColumns(JSON.parse(JSON.stringify(columns)));
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// do not not show the foreign-key column of the parent table // // do not not show the foreign-key column of the parent table //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
if(data.defaultValuesForNewChildRecords) if (data.defaultValuesForNewChildRecords)
{ {
for (let i = 0; i < columns.length; i++) for (let i = 0; i < columns.length; i++)
{ {
if(data.defaultValuesForNewChildRecords[columns[i].field]) if (data.defaultValuesForNewChildRecords[columns[i].field])
{ {
columns.splice(i, 1); columns.splice(i, 1);
i-- i--;
} }
} }
} }
@ -131,7 +131,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
//////////////////////////////////// ////////////////////////////////////
// add actions cell, if available // // add actions cell, if available //
//////////////////////////////////// ////////////////////////////////////
if(allowRecordEdit || allowRecordDelete) if (allowRecordEdit || allowRecordDelete)
{ {
columns.unshift({ columns.unshift({
field: "_actions", field: "_actions",
@ -145,19 +145,19 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
return <Box> return <Box>
{allowRecordEdit && <IconButton onClick={() => editRecordCallback(params.row.__rowIndex)}><Icon>edit</Icon></IconButton>} {allowRecordEdit && <IconButton onClick={() => editRecordCallback(params.row.__rowIndex)}><Icon>edit</Icon></IconButton>}
{allowRecordDelete && <IconButton onClick={() => deleteRecordCallback(params.row.__rowIndex)}><Icon>delete</Icon></IconButton>} {allowRecordDelete && <IconButton onClick={() => deleteRecordCallback(params.row.__rowIndex)}><Icon>delete</Icon></IconButton>}
</Box> </Box>;
}) })
}) });
} }
setRows(rows); setRows(rows);
setRecords(records) setRecords(records);
setColumns(columns); setColumns(columns);
let csv = ""; let csv = "";
for (let i = 0; i < allColumns.length; i++) for (let i = 0; i < allColumns.length; i++)
{ {
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"` csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`;
} }
csv += "\n"; csv += "\n";
@ -165,8 +165,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
{ {
for (let j = 0; j < allColumns.length; j++) for (let j = 0; j < allColumns.length; j++)
{ {
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field) const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field);
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"` csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`;
} }
csv += "\n"; csv += "\n";
} }
@ -182,13 +182,13 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
// view all link // // view all link //
/////////////////// ///////////////////
const labelAdditionalElementsLeft: JSX.Element[] = []; const labelAdditionalElementsLeft: JSX.Element[] = [];
if(data && data.viewAllLink) if (data && data.viewAllLink)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography key={"viewAllLink"} variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative"> <Typography key={"viewAllLink"} variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
<Link to={data.viewAllLink}>View All</Link> <Link to={data.viewAllLink}>View All</Link>
</Typography> </Typography>
) );
} }
/////////////////// ///////////////////
@ -200,10 +200,10 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
{ {
isExportDisabled = false; isExportDisabled = false;
if(data.totalRows && data.queryOutput.records.length < data.totalRows) if (data.totalRows && data.queryOutput.records.length < data.totalRows)
{ {
tooltipTitle = "Export these " + data.queryOutput.records.length + " records." tooltipTitle = "Export these " + data.queryOutput.records.length + " records.";
if(data.viewAllLink) if (data.viewAllLink)
{ {
tooltipTitle += "\nClick View All to export all records."; tooltipTitle += "\nClick View All to export all records.";
} }
@ -212,17 +212,17 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
const onExportClick = () => const onExportClick = () =>
{ {
if(csv) if (csv)
{ {
HtmlUtils.download(fileName, csv); HtmlUtils.download(fileName, csv);
} }
else else
{ {
alert("There is no data available to export.") alert("There is no data available to export.");
} }
} };
if(widgetMetaData?.showExportButton) if (widgetMetaData?.showExportButton)
{ {
labelAdditionalElementsLeft.push( labelAdditionalElementsLeft.push(
<Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative"> <Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative">
@ -234,15 +234,15 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
//////////////////// ////////////////////
// add new button // // add new button //
//////////////////// ////////////////////
const labelAdditionalComponentsRight: LabelComponent[] = [] const labelAdditionalComponentsRight: LabelComponent[] = [];
if(data && data.canAddChildRecord) if (data && data.canAddChildRecord)
{ {
let disabledFields = data.disabledFieldsForNewChildRecords; let disabledFields = data.disabledFieldsForNewChildRecords;
if(!disabledFields) if (!disabledFields)
{ {
disabledFields = data.defaultValuesForNewChildRecords; disabledFields = data.defaultValuesForNewChildRecords;
} }
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback)) labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
} }
@ -251,16 +251,16 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) => const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
{ {
if(disableRowClick) if (disableRowClick)
{ {
return; return;
} }
(async () => (async () =>
{ {
const qInstance = await qController.loadMetaData() const qInstance = await qController.loadMetaData();
let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name) let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name);
if(tablePath) if (tablePath)
{ {
tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`; tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`;
DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance); DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance);
@ -276,7 +276,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
*******************************************************************************/ *******************************************************************************/
function CustomToolbar() function CustomToolbar()
{ {
const handleMouseDown: GridEventListener<"cellMouseDown"> = ( params, event, details ) => const handleMouseDown: GridEventListener<"cellMouseDown"> = (params, event, details) =>
{ {
setGridMouseDownX(event.clientX); setGridMouseDownX(event.clientX);
setGridMouseDownY(event.clientY); setGridMouseDownY(event.clientY);
@ -304,8 +304,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
labelAdditionalComponentsRight={labelAdditionalComponentsRight} labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}} labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
> >
<Box mx={-2} mb={-3}> <Box mx={-3} mb={-3}>
<Box className="recordGridWidget"> <Box>
<DataGridPro <DataGridPro
autoHeight autoHeight
sx={{ sx={{

View File

@ -19,15 +19,12 @@
*/ */
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {tooltipClasses, TooltipProps} from "@mui/material"; import {Box, tooltipClasses, TooltipProps} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {styled} from "@mui/material/styles"; import {styled} from "@mui/material/styles";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer"; import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import parse from "html-react-parser"; import parse from "html-react-parser";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
@ -166,7 +163,7 @@ function DataTable({
})} })}
> >
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */} {/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon> <Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_left"}</Icon>
</span> </span>
) : null, ) : null,
}, },
@ -312,7 +309,7 @@ function DataTable({
{ {
boxStyle = isFooter boxStyle = isFooter
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"} ? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"}; : {height: fixedHeight ? `${fixedHeight}px` : "auto", flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
} }
let innerBoxStyle = {}; let innerBoxStyle = {};
@ -321,143 +318,139 @@ function DataTable({
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"}; innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
} }
///////////////////////////////////////////////////////////////////////////////////
// note - at one point, we had the table's sx including: whiteSpace: "nowrap"... //
///////////////////////////////////////////////////////////////////////////////////
return <Box sx={boxStyle}><Box sx={innerBoxStyle}> return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
<Table {...getTableProps()}> <Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
{ {
includeHead && ( includeHead && (
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}> headerGroups.map((headerGroup: any, i: number) => (
{headerGroups.map((headerGroup: any, i: number) => ( headerGroup.headers.map((column: any) => (
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}> column.type !== "hidden" && (
{headerGroup.headers.map((column: any) => ( <DataTableHeadCell
column.type !== "hidden" && ( sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
<DataTableHeadCell key={i++}
key={i++} {...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())} align={column.align ? column.align : "left"}
align={column.align ? column.align : "left"} sorted={setSortedValue(column)}
sorted={setSortedValue(column)} tooltip={column.tooltip}
tooltip={column.tooltip} >
> {column.render("header")}
{column.render("header")} </DataTableHeadCell>
</DataTableHeadCell> )
) ))
))} ))
</TableRow>
))}
</Box>
) )
} }
<TableBody {...getTableBodyProps()}> {rows.map((row: any, key: any) =>
{rows.map((row: any, key: any) => {
prepareRow(row);
let overrideNoEndBorder = false;
//////////////////////////////////////////////////////////////////////////////////
// don't do an end-border on nested rows - unless they're the last one in a set //
//////////////////////////////////////////////////////////////////////////////////
if (row.depth > 0)
{ {
prepareRow(row); overrideNoEndBorder = true;
if (key + 1 < rows.length && rows[key + 1].depth == 0)
let overrideNoEndBorder = false;
//////////////////////////////////////////////////////////////////////////////////
// don't do an end-border on nested rows - unless they're the last one in a set //
//////////////////////////////////////////////////////////////////////////////////
if (row.depth > 0)
{ {
overrideNoEndBorder = true; overrideNoEndBorder = false;
if (key + 1 < rows.length && rows[key + 1].depth == 0)
{
overrideNoEndBorder = false;
}
} }
}
/////////////////////////////////////// ///////////////////////////////////////
// don't do end-border on the footer // // don't do end-border on the footer //
/////////////////////////////////////// ///////////////////////////////////////
if (isFooter) if (isFooter)
{ {
overrideNoEndBorder = true; overrideNoEndBorder = true;
} }
let background = "initial"; let background = "initial";
if (isFooter) if (isFooter)
{ {
background = "#EEEEEE"; background = "#EEEEEE";
} }
else if (row.depth > 0 || row.isExpanded) else if (row.depth > 0 || row.isExpanded)
{ {
background = "#FAFAFA"; background = "#FAFAFA";
} }
return ( return (
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}> row.cells.map((cell: any) => (
{row.cells.map((cell: any) => ( cell.column.type !== "hidden" && (
cell.column.type !== "hidden" && ( <DataTableBodyCell
<DataTableBodyCell key={key}
key={key} sx={{verticalAlign: "top", background: background}}
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded} noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
depth={row.depth} depth={row.depth}
align={cell.column.align ? cell.column.align : "left"} align={cell.column.align ? cell.column.align : "left"}
{...cell.getCellProps()} {...cell.getCellProps()}
> >
{ {
cell.column.type === "default" && ( cell.column.type === "default" && (
cell.value && "number" === typeof cell.value ? ( cell.value && "number" === typeof cell.value ? (
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell> <DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>) ) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
) )
} }
{ {
cell.column.type === "htmlAndTooltip" && ( cell.column.type === "htmlAndTooltip" && (
<DefaultCell isFooter={isFooter}> <DefaultCell isFooter={isFooter}>
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}> <NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
<Box> <Box>
{parse(cell.value)} {parse(cell.value)}
</Box> </Box>
</NoMaxWidthTooltip> </NoMaxWidthTooltip>
</DefaultCell> </DefaultCell>
) )
} }
{ {
cell.column.type === "html" && ( cell.column.type === "html" && (
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell> <DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
) )
} }
{ {
cell.column.type === "composite" && ( cell.column.type === "composite" && (
<DefaultCell isFooter={isFooter}> <DefaultCell isFooter={isFooter}>
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} /> <CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
</DefaultCell> </DefaultCell>
) )
} }
{ {
cell.column.type === "block" && ( cell.column.type === "block" && (
<DefaultCell isFooter={isFooter}> <DefaultCell isFooter={isFooter}>
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} /> <WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
</DefaultCell> </DefaultCell>
) )
} }
{ {
cell.column.type === "image" && row.values["imageTotal"] && ( cell.column.type === "image" && row.values["imageTotal"] && (
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} /> <ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
) )
} }
{ {
cell.column.type === "image" && !row.values["imageTotal"] && ( cell.column.type === "image" && !row.values["imageTotal"] && (
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} /> <ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
) )
} }
{ {
(cell.column.id === "__expander") && cell.render("cell") (cell.column.id === "__expander") && cell.render("cell")
} }
</DataTableBodyCell> </DataTableBodyCell>
) )
))} ))
</TableRow> );
); })}
})}
</TableBody>
</Table> </Table>
</Box></Box>; </Box></Box>;
} }
return ( return (
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}> <TableContainer sx={{boxShadow: "none", height: (fixedHeight && !fixedStickyLastRow) ? `${fixedHeight}px` : "auto"}}>
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? ( {entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}> <Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && ( {entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (

View File

@ -93,41 +93,25 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
/> />
: noRowsFoundHTML ? : noRowsFoundHTML ?
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}> <Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
<MDTypography <MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
variant="subtitle2" {noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
color="secondary"
fontWeight="regular"
>
{
noRowsFoundHTML ? (
parse(noRowsFoundHTML)
) : "No rows found"
}
</MDTypography> </MDTypography>
</Box> </Box>
: :
<TableContainer sx={{boxShadow: "none"}}> <TableContainer sx={{boxShadow: "none"}}>
<Table> <Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
<Box component="thead"> {Array(8).fill(0).map((_, i) =>
<TableRow sx={{alignItems: "flex-end"}} key="header"> <DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
{Array(8).fill(0).map((_, i) => <Skeleton width="100%" />
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center"> </DataTableHeadCell>
<Skeleton width="100%" /> )}
</DataTableHeadCell> {Array(5).fill(0).map((_, i) =>
)} Array(8).fill(0).map((_, j) =>
</TableRow> <DataTableBodyCell key={`cell-${i}-${j}`} align="center">
</Box> <DefaultCell isFooter={false}><Skeleton /></DefaultCell>
<TableBody> </DataTableBodyCell>
{Array(5).fill(0).map((_, i) => )
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}> )}
{Array(8).fill(0).map((_, j) =>
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
</DataTableBodyCell>
)}
</TableRow>
)}
</TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
} }

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import Box from "@mui/material/Box"; import {Box} from "@mui/material";
import {Theme} from "@mui/material/styles"; import {Theme} from "@mui/material/styles";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import {ReactNode} from "react"; import {ReactNode} from "react";
@ -30,13 +30,14 @@ interface Props
children: ReactNode; children: ReactNode;
noBorder?: boolean; noBorder?: boolean;
align?: "left" | "right" | "center"; align?: "left" | "right" | "center";
sx?: any;
} }
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
{ {
return ( return (
<Box <Box
component="td" component="div"
textAlign={align} textAlign={align}
py={1.5} py={1.5}
px={1.5} px={1.5}
@ -54,7 +55,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
}, },
"&:last-child": { "&:last-child": {
paddingRight: "1rem" paddingRight: "1rem"
} }, ...sx
})} })}
> >
<Box <Box
@ -72,6 +73,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
DataTableBodyCell.defaultProps = { DataTableBodyCell.defaultProps = {
noBorder: false, noBorder: false,
align: "left", align: "left",
sx: {}
}; };
export default DataTableBodyCell; export default DataTableBodyCell;

View File

@ -44,18 +44,14 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
return ( return (
<Box <Box
component="th" component="div"
width={width} width={width}
py={1.5} py={1.5}
px={1.5} px={1.5}
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({ sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`, borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
"&:nth-of-type(1)": { position: "sticky", top: 0, background: "white",
paddingLeft: "1rem" zIndex: 1 // so if body rows scroll behind it, they don't show through
},
"&:last-child": {
paddingRight: "1rem"
},
})} })}
> >
<Box <Box

View File

@ -33,6 +33,7 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
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 {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted"; import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material"; import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
@ -96,6 +97,8 @@ let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: b
{ {
}; };
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
{ {
const processNameParam = useParams().processName; const processNameParam = useParams().processName;
@ -443,7 +446,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
if (processValues[key]) if (processValues[key])
{ {
formFields[key].possibleValueProps.initialDisplayValue = processValues[key]; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have a cached possible-value label for this field name (key), then set it as the PV's initialDisplayValue //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (cachedPossibleValueLabels[key] && cachedPossibleValueLabels[key][processValues[key]])
{
formFields[key].possibleValueProps.initialDisplayValue = cachedPossibleValueLabels[key][processValues[key]];
}
else
{
////////////////////////////////////////////////////////////////////////////
// else (and i don't think this should happen?) at least set something... //
////////////////////////////////////////////////////////////////////////////
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
}
} }
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>(); formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
@ -865,6 +881,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
dynamicFormFields[fieldName] = dynamicFormValue; dynamicFormFields[fieldName] = dynamicFormValue;
initialValues[fieldName] = initialValue; initialValues[fieldName] = initialValue;
if (formikSetFieldValueFunction)
{
formikSetFieldValueFunction(fieldName, initialValue);
}
formValidations[fieldName] = validation; formValidations[fieldName] = validation;
}; };
@ -914,6 +936,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
fullFieldList.forEach((field) => fullFieldList.forEach((field) =>
{ {
initialValues[field.name] = processValues[field.name]; initialValues[field.name] = processValues[field.name];
if (formikSetFieldValueFunction)
{
formikSetFieldValueFunction(field.name, processValues[field.name]);
}
}); });
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
@ -1073,40 +1100,88 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (lastProcessResponse instanceof QJobComplete) if (lastProcessResponse instanceof QJobComplete)
{ {
const qJobComplete = lastProcessResponse as QJobComplete; ///////////////////////////////////////////////////////////////////////////////////////////////
setJobUUID(null); // run an async function here, in case we need to await looking up any possible-value labels //
setNewStep(qJobComplete.nextStep); ///////////////////////////////////////////////////////////////////////////////////////////////
setProcessValues(qJobComplete.values); (async () =>
setQJobRunning(null);
if (formikSetFieldValueFunction)
{ {
////////////////////////////////// const qJobComplete = lastProcessResponse as QJobComplete;
// reset field values in formik // const newValues = qJobComplete.values;
//////////////////////////////////
for (let key in qJobComplete.values) /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let frontendSteps = steps;
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
if (updatedFrontendStepList)
{ {
if (Object.hasOwn(formFields, key)) setSteps(updatedFrontendStepList);
frontendSteps = updatedFrontendStepList;
}
///////////////////////////////////////////////////////////////////////////////////
// if the next screen has any PVS fields - look up their labels (display values) //
///////////////////////////////////////////////////////////////////////////////////
const nextStepName = qJobComplete.nextStep;
let nextStep: QFrontendStepMetaData | null = null;
if (frontendSteps && nextStepName)
{
for (let i = 0; i < frontendSteps.length; i++)
{ {
console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`); if (frontendSteps[i].name === nextStepName)
formikSetFieldValueFunction(key, qJobComplete.values[key]); {
nextStep = frontendSteps[i];
break;
}
}
if (nextStep && nextStep.formFields)
{
for (let i = 0; i < nextStep.formFields.length; i++)
{
const field = nextStep.formFields[i];
const fieldName = field.name;
if (field.possibleValueSourceName && newValues && newValues[fieldName])
{
const results: QPossibleValue[] = await Client.getInstance().possibleValues(null, processName, fieldName, null, [newValues[fieldName]]);
if (results && results.length > 0)
{
if (!cachedPossibleValueLabels[fieldName])
{
cachedPossibleValueLabels[fieldName] = {};
}
cachedPossibleValueLabels[fieldName][newValues[fieldName]] = results[0].label;
}
}
}
} }
} }
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// setJobUUID(null);
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) // setNewStep(nextStepName);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// setProcessValues(newValues);
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList; setQJobRunning(null);
if (updatedFrontendStepList)
{
setSteps(updatedFrontendStepList);
}
if (activeStep && activeStep.recordListFields) if (formikSetFieldValueFunction)
{ {
setNeedRecords(true); //////////////////////////////////
} // reset field values in formik //
//////////////////////////////////
for (let key in qJobComplete.values)
{
if (Object.hasOwn(formFields, key))
{
console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`);
formikSetFieldValueFunction(key, qJobComplete.values[key]);
}
}
}
if (activeStep && activeStep.recordListFields)
{
setNeedRecords(true);
}
})();
} }
else if (lastProcessResponse instanceof QJobStarted) else if (lastProcessResponse instanceof QJobStarted)
{ {
@ -1348,7 +1423,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const formData = new FormData(); const formData = new FormData();
Object.keys(values).forEach((key) => Object.keys(values).forEach((key) =>
{ {
formData.append(key, values[key]); if (values[key] !== undefined)
{
formData.append(key, values[key]);
}
}); });
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey)) if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
@ -1571,7 +1649,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
); );
const body = ( const body = (
<Box py={3} mb={20}> <Box py={3} mb={20} className="processRun">
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}> <Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
<Grid item xs={12} lg={10} xl={8}> <Grid item xs={12} lg={10} xl={8}>
{form} {form}

View File

@ -121,7 +121,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
} }
const valueCounts = [] as QRecord[]; const valueCounts = [] as QRecord[];
for(let i = 0; i < result.values.valueCounts.length; i++) for(let i = 0; i < result.values.valueCounts?.length; i++)
{ {
let valueRecord = new QRecord(result.values.valueCounts[i]); let valueRecord = new QRecord(result.values.valueCounts[i]);

View File

@ -786,6 +786,7 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
initialDisplayValue={selectedPossibleValue?.label} initialDisplayValue={selectedPossibleValue?.label}
inForm={false} inForm={false}
onChange={handleChange} onChange={handleChange}
useCase="filter"
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}} // InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
/> />
</Box> </Box>
@ -854,6 +855,7 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
initialValues={selectedPossibleValues} initialValues={selectedPossibleValues}
inForm={false} inForm={false}
onChange={handleChange} onChange={handleChange}
useCase="filter"
/> />
</Box> </Box>
); );

View File

@ -33,8 +33,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy"; import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Collapse, Menu, Typography} from "@mui/material"; import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
@ -92,8 +91,10 @@ interface Props
launchProcess?: QProcessMetaData; launchProcess?: QProcessMetaData;
usage?: QueryScreenUsage; usage?: QueryScreenUsage;
isModal?: boolean; isModal?: boolean;
isPreview?: boolean;
initialQueryFilter?: QQueryFilter; initialQueryFilter?: QQueryFilter;
initialColumns?: QQueryColumns; initialColumns?: QQueryColumns;
allowVariables?: boolean;
} }
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
@ -125,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
** **
** Yuge component. The best. Lots of very smart people are saying so. ** Yuge component. The best. Lots of very smart people are saying so.
*******************************************************************************/ *******************************************************************************/
const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, initialColumns}: Props, ref) => const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
{ {
const tableName = table.name; const tableName = table.name;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -630,7 +631,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
const type = (e.target as any).type; const type = (e.target as any).type;
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search"); const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
if (validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess) if (validType && !isModal && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
{ {
if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{ {
@ -668,7 +669,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
{ {
document.removeEventListener("keydown", down); document.removeEventListener("keydown", down);
}; };
}, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]); }, [isModal, dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]);
/******************************************************************************* /*******************************************************************************
@ -883,6 +884,18 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
}; };
/*******************************************************************************
** Opens a new query screen in a new window with the current filter
*******************************************************************************/
const openFilterInNewWindow = () =>
{
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
const url = `${metaData?.getTablePathByName(tableName)}?filter=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
window.open(url);
};
/******************************************************************************* /*******************************************************************************
** This is the method that actually executes a query to update the data in the table. ** This is the method that actually executes a query to update the data in the table.
*******************************************************************************/ *******************************************************************************/
@ -2231,12 +2244,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
return ( return (
<GridToolbarContainer> <GridToolbarContainer>
<div> <div>
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button> <Tooltip title="Refresh Query">
</div> <Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
<div style={{position: "relative"}}> </Tooltip>
{/* @ts-ignore */}
<GridToolbarDensitySelector nonce={undefined} />
</div> </div>
{
!isPreview && (
<div style={{position: "relative"}}>
{/* @ts-ignore */}
<GridToolbarDensitySelector nonce={undefined} />
</div>
)
}
{
isPreview && (
<Tooltip title="Open In New Window">
<Button id="open-filter-in-new-window-button" onClick={() => openFilterInNewWindow()} startIcon={<Icon>launch</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
</Tooltip>
)
}
{ {
usage == "queryScreen" && usage == "queryScreen" &&
@ -2871,7 +2897,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
} }
{ {
metaData && tableMetaData && !isPreview && metaData && tableMetaData &&
<BasicAndAdvancedQueryControls <BasicAndAdvancedQueryControls
ref={basicAndAdvancedQueryControlsRef} ref={basicAndAdvancedQueryControlsRef}
metaData={metaData} metaData={metaData}
@ -2884,6 +2910,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
gridApiRef={gridApiRef} gridApiRef={gridApiRef}
mode={mode} mode={mode}
queryScreenUsage={usage} queryScreenUsage={usage}
allowVariables={allowVariables}
setMode={doSetMode} setMode={doSetMode}
savedViewsComponent={savedViewsComponent} savedViewsComponent={savedViewsComponent}
columnMenuComponent={buildColumnMenu()} columnMenuComponent={buildColumnMenu()}
@ -2912,6 +2939,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
metaData: metaData, metaData: metaData,
queryFilter: queryFilter, queryFilter: queryFilter,
updateFilter: doSetQueryFilter, updateFilter: doSetQueryFilter,
allowVariables: allowVariables
} }
}} }}
localeText={{ localeText={{

View File

@ -421,7 +421,7 @@ input[type="search"]::-webkit-search-results-decoration
font-size: 2rem !important; font-size: 2rem !important;
} }
.dashboard-order-release-icon .dashboard-table-actions-icon
{ {
font-size: 1.5rem !important; font-size: 1.5rem !important;
position: relative; position: relative;
@ -711,17 +711,13 @@ input[type="search"]::-webkit-search-results-decoration
padding: 24px; padding: 24px;
} }
.recordView .widget .recordGridWidget
{
margin: -8px;
}
.MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover .MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover
{ {
color: white; color: white;
background-color: #0062FF !important; background-color: #0062FF !important;
} }
/* several styles below here for user-defined alert inside helpContent */
.helpContentAlert .helpContentAlert
{ {
padding: 6px 16px; padding: 6px 16px;
@ -784,3 +780,27 @@ input[type="search"]::-webkit-search-results-decoration
{ {
color: #F44335; color: #F44335;
} }
/* the alert widget, was built with minimal (no?) margins, for embedding in
a parent widget; but for using it on a process, give it some breathing room */
.processRun .widget .MuiAlert-root
{
margin: 2rem 1rem;
}
/* default styles for a block widget overlay */
.blockWidgetOverlay
{
font-weight: 400;
position: relative;
top: 15px;
height: 0;
display: flex;
font-size: 14px;
flex-direction: column;
align-items: center;
}
.blockWidgetOverlay a
{
color: #0062FF !important;
}

View File

@ -268,7 +268,15 @@ class ValueUtils
{ {
if (!(date instanceof Date)) if (!(date instanceof Date))
{ {
////////////////////////////////////////////////////////////////////////////////////
// so, a new Date here will interpret the string as being at midnight UTC, but //
// the data object will be in the user/browser timezone. //
// so "2024-08-22", for a user in US/Central, will be "2024-08-21T19:00:00-0500". //
// correct for that by adding the date's timezone offset (converted from minutes //
// to millis) back to it //
////////////////////////////////////////////////////////////////////////////////////
date = new Date(date); date = new Date(date);
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000)
} }
// @ts-ignore // @ts-ignore
return (`${date.toString("yyyy-MM-dd")}`); return (`${date.toString("yyyy-MM-dd")}`);

View File

@ -56,8 +56,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
"label": "Sample Table Widget", "label": "Sample Table Widget",
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT", "footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
"columns": [ "columns": [
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" }, { "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" } { "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
], ],
"rows": [ "rows": [
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" }, { "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
@ -83,7 +83,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
// assert that the table widget rendered its header and some contents // // assert that the table widget rendered its header and some contents //
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget"); qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S."); qSeleniumLib.waitForSelectorContaining("#SampleTableWidget a", "Homer S.");
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT"); qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
///////////////////////////// /////////////////////////////