Compare commits

..

18 Commits

Author SHA1 Message Date
34c6f650b5 updated to handle (ignore) fields with empty strings when using goto dialog 2025-04-07 16:50:01 -05:00
a6ee682671 Merged feature/dk-misc-20250318 into dev 2025-04-03 14:28:34 -05:00
c62252075f Merged feature/banners into dev 2025-04-03 14:26:13 -05:00
679375ba63 update processClicked to set alert if min/max input records isn't satisfied 2025-03-18 11:44:32 -05:00
fb10dad803 Add support for query-param defaultProcessValues (as a json object) 2025-03-18 11:40:22 -05:00
c9a618c7f6 Fix full-width file upload adornment for lg-size (regressed with field-level grid columns addition) 2025-03-10 12:12:37 -05:00
13ce684d23 Initial checkin of Banners under QBrandingMetaData
- includes migration from (now deprecated) MetaDataFilterInterface to MetaDataActionCustomizerInterface (stored on the QInstance and used by MetaDataAction)
- includes migration from (now deprecated) environmentBannerText and environmentBannerColor in QBrandingMetaData to now be implemented as a banner
2025-03-07 14:58:51 -06:00
8ae3b95105 Merge tag 'version-0.24.0' into dev
Tag release
2025-03-06 11:20:23 -06:00
b67eea7d87 Merge branch 'release/0.24.0' 2025-03-06 11:20:23 -06:00
5a309e5628 Update for next development version 2025-03-06 11:04:18 -06:00
67e1e78817 Update versions for release 2025-03-06 11:04:17 -06:00
214b6b8af4 Merged feature/sftp-and-headless-bulk-load into dev 2025-03-05 19:45:22 -06:00
8ec0ce5455 Cleanup from code review 2025-03-05 19:30:52 -06:00
07cb6fd323 Fix show blob download urls when not using dynamic url 2025-02-25 10:55:49 -06:00
3bb8451671 add support for toRecordFromTableDynamic in LINK adornment, and downloadUrlDynamic in FILE_DOWNLOAD adornment 2025-02-24 11:10:36 -06:00
44a8810260 Remove textTransform="capitalize" from pageHeader h3 2025-02-14 20:37:12 -06:00
c69a4b8203 Make variants work for blob/download fields 2025-02-14 20:36:49 -06:00
7db4f34ddd add LONG to types that get numeric operators 2025-02-14 20:34:59 -06:00
20 changed files with 363 additions and 55 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
.yalc*
yalc.lock
.env
/certs
# dependencies
/node_modules

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.114",
"@kingsrook/qqq-frontend-core": "1.0.119",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",

View File

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

View File

@ -29,6 +29,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon";
import {ThemeProvider} from "@mui/material/styles";
@ -38,6 +39,7 @@ import jwt_decode from "jwt-decode";
import QContext from "QContext";
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
import theme from "qqq/components/legacy/Theme";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context";
import AppHome from "qqq/pages/apps/Home";
import NoApps from "qqq/pages/apps/NoApps";
@ -691,6 +693,23 @@ export default function App()
}
/***************************************************************************
**
***************************************************************************/
function banner(): JSX.Element | null
{
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_SITE");
if (!banner)
{
return (null);
}
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", position: "sticky", top: "0", zIndex: 1, ...getBannerStyles(banner)}}>
{makeBannerContent(banner)}
</Box>);
}
return (
appRoutes && (
@ -718,6 +737,7 @@ export default function App()
<ThemeProvider theme={theme}>
<CssBaseline />
<CommandMenu metaData={metaData} />
{banner()}
<Sidenav
color={sidenavColor}
icon={branding.icon}

View File

@ -0,0 +1,36 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
/*******************************************************************************
**
*******************************************************************************/
public enum MaterialDashboardBannerSlots implements BannerSlot
{
QFMD_TOP_OF_SITE,
QFMD_TOP_OF_BODY,
QFMD_SIDE_NAV_UNDER_LOGO
}

View File

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

View File

@ -270,7 +270,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
{
pageHeader &&
<Box display="flex" justifyContent="space-between">
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
<MDTypography pb="0.5rem" variant="h3" color={light ? "white" : "dark"} noWrap>
{pageHeader}
</MDTypography>
</Box>

View File

@ -34,6 +34,7 @@ import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
import MDTypography from "qqq/components/legacy/MDTypography";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
@ -300,6 +301,30 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
}
);
/***************************************************************************
**
***************************************************************************/
function EnvironmentBanner({branding}: { branding: QBrandingMetaData }): JSX.Element | null
{
// deprecated!
if (branding && branding.environmentBannerText)
{
return <Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
{branding.environmentBannerText}
</Box>;
}
const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO");
if (banner)
{
return <Box className={getBannerClassName(banner)} mt={2} borderRadius={2} sx={getBannerStyles(banner)}>
{makeBannerContent(banner)}
</Box>;
}
return (null);
}
return (
<SidenavRoot
{...rest}
@ -330,12 +355,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
</Box>
}
</Box>
{
branding && branding.environmentBannerText &&
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
{branding.environmentBannerText}
</Box>
}
<EnvironmentBanner branding={branding} />
</Box>
<Divider
light={

View File

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

View File

@ -0,0 +1,97 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Banner} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Banner";
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
import parse from "html-react-parser";
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// One may render a banner using the functions in this file as: //
// //
// const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO"); //
// return (<Box className={getBannerClassName(banner)} sx={{padding: "1rem", ...getBannerStyles(banner)}}> //
// {makeBannerContent(banner)} //
// </Box>); //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/***************************************************************************
**
***************************************************************************/
export function getBanner(branding: QBrandingMetaData, slot: string): Banner | null
{
if (branding?.banners?.has(slot))
{
return (branding.banners.get(slot));
}
return (null);
}
/***************************************************************************
**
***************************************************************************/
export function getBannerStyles(banner: Banner)
{
let bgColor = "";
let color = "";
if (banner)
{
if (banner.backgroundColor)
{
bgColor = banner.backgroundColor;
}
if (banner.textColor)
{
bgColor = banner.textColor;
}
}
const rest = banner?.additionalStyles ?? {};
return ({
backgroundColor: bgColor,
color: color,
...rest
});
}
/***************************************************************************
**
***************************************************************************/
export function getBannerClassName(banner: Banner)
{
return `banner ${banner?.severity?.toLowerCase()}`;
}
/***************************************************************************
**
***************************************************************************/
export function makeBannerContent(banner: Banner): JSX.Element
{
return <>{banner?.messageHTML ? parse(banner?.messageHTML) : banner?.messageText}</>;
}

View File

@ -20,6 +20,7 @@
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
@ -35,12 +36,11 @@ import DialogTitle from "@mui/material/DialogTitle";
import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField";
import {any} from "prop-types";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
import MDButton from "qqq/components/legacy/MDButton";
import Client from "qqq/utils/qqq/Client";
import React, {useState} from "react";
import {useNavigate} from "react-router-dom";
interface Props
{
@ -162,8 +162,8 @@ function GotoRecordDialog(props: Props): JSX.Element
/***************************************************************************
** event handler for close button
***************************************************************************/
** event handler for close button
***************************************************************************/
const closeRequested = () =>
{
if (props.mayClose)
@ -182,23 +182,23 @@ function GotoRecordDialog(props: Props): JSX.Element
options[optionIndex].forEach((field) =>
{
if(values[field.name])
if (values[field.name])
{
anyFieldsInThisOptionHaveAValue = true;
}
})
});
if(!anyFieldsInThisOptionHaveAValue)
if (!anyFieldsInThisOptionHaveAValue)
{
return (true);
}
return (false);
}
};
/***************************************************************************
** event handler for clicking an 'option's go/submit button
***************************************************************************/
** event handler for clicking an 'option's go/submit button
***************************************************************************/
const optionGoClicked = async (optionIndex: number) =>
{
setError("");
@ -207,9 +207,13 @@ function GotoRecordDialog(props: Props): JSX.Element
const queryStringParts: string[] = [];
options[optionIndex].forEach((field) =>
{
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
})
if (field.type == QFieldType.STRING && !values[field.name][0])
{
return;
}
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]));
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`);
});
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
@ -223,7 +227,7 @@ function GotoRecordDialog(props: Props): JSX.Element
}
else if (queryResult.length == 1)
{
if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
if (options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
{
/////////////////////////////////////////////////
// navigate by pkey, if that's how we searched //

View File

@ -109,6 +109,7 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
{
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
case QFieldType.LONG:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});

View File

@ -111,7 +111,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
}
const tableMetaData = data.childTableMetaData instanceof QTableMetaData ? data.childTableMetaData as QTableMetaData : new QTableMetaData(data.childTableMetaData);
const rows = DataGridUtils.makeRows(records, tableMetaData, true);
const rows = DataGridUtils.makeRows(records, tableMetaData, undefined, true);
/////////////////////////////////////////////////////////////////////////////////
// note - tablePath may be null, if the user doesn't have access to the table. //
@ -255,14 +255,14 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
disabledFields = data.defaultValuesForNewChildRecords;
}
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {}
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {};
///////////////////////////////////////////////////////////////////////////////////////
// copy values from specified fields in the parent record down into the child record //
///////////////////////////////////////////////////////////////////////////////////////
if(data.defaultValuesForNewChildRecordsFromParentFields)
if (data.defaultValuesForNewChildRecordsFromParentFields)
{
for(let childField in data.defaultValuesForNewChildRecordsFromParentFields)
for (let childField in data.defaultValuesForNewChildRecordsFromParentFields)
{
const parentField = data.defaultValuesForNewChildRecordsFromParentFields[childField];
defaultValuesForNewChildRecords[childField] = parentRecord?.values?.get(parentField);

View File

@ -21,11 +21,12 @@
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import Box from "@mui/material/Box";
import {ReactNode, useEffect, useState} from "react";
import Footer from "qqq/components/horseshoe/Footer";
import NavBar from "qqq/components/horseshoe/NavBar";
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
import DashboardLayout from "qqq/layouts/DashboardLayout";
import Client from "qqq/utils/qqq/Client";
import {ReactNode, useEffect, useState} from "react";
interface Props
{
@ -80,12 +81,34 @@ function BaseLayout({stickyNavbar, children}: Props): JSX.Element
return () => window.removeEventListener("resize", handleTabsOrientation);
}, [tabsOrientation]);
/***************************************************************************
**
***************************************************************************/
function banner(): JSX.Element | null
{
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_BODY");
if (!banner)
{
return (null);
}
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", margin: "-20px", marginBottom: "20px", ...getBannerStyles(banner)}}>
{makeBannerContent(banner)}
</Box>);
}
return (
<DashboardLayout>
<NavBar />
<Box>{children}</Box>
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
</DashboardLayout>
<>
<DashboardLayout>
{banner()}
<NavBar />
<Box>{children}</Box>
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
</DashboardLayout>
</>
);
}

View File

@ -1838,6 +1838,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
return;
}
if(urlSearchParams.get("defaultProcessValues"))
{
if(!defaultProcessValues)
{
defaultProcessValues = {}
}
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
for (let key in values)
{
defaultProcessValues[key] = values[key]
}
}
if (defaultProcessValues)
{
for (let key in defaultProcessValues)

View File

@ -1103,7 +1103,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
////////////////////////////////
// make the rows for the grid //
////////////////////////////////
const rows = DataGridUtils.makeRows(results, tableMetaData);
const rows = DataGridUtils.makeRows(results, tableMetaData, tableVariant);
setRows(rows);
setLoading(false);
@ -1612,6 +1612,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
*******************************************************************************/
const processClicked = (process: QProcessMetaData) =>
{
if (process.minInputRecords != null && process.minInputRecords > 0 && getNoOfSelectedRecords() === 0)
{
setAlertContent(`No records were selected for the process: ${process.label}`);
return;
}
else if (process.minInputRecords != null && getNoOfSelectedRecords() < process.minInputRecords)
{
setAlertContent(`Too few records were selected for the process: ${process.label}. A minimum of ${process.minInputRecords} is required.`);
return;
}
else if (process.maxInputRecords != null && getNoOfSelectedRecords() > process.maxInputRecords)
{
setAlertContent(`Too many records were selected for the process: ${process.label}. A maximum of ${process.maxInputRecords} is allowed.`);
return;
}
// todo - let the process specify that it needs initial rows - err if none selected.
// alternatively, let a process itself have an initial screen to select rows...
openModalProcess(process);

View File

@ -92,7 +92,7 @@ const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
/*******************************************************************************
**
*******************************************************************************/
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: { label?: SxProps, value?: SxProps })
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps}, tableVariant?: QTableVariant)
{
return <Grid container lg={12} key={key} display="flex" py={1} pr={2}>
{
@ -119,7 +119,7 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
}
<div style={{display: "inline-block", width: 0}}>&nbsp;</div>
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)" sx={{...(styleOverrides?.value ?? {})}}>
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
{ValueUtils.getDisplayValue(field, record, "view", fieldName, tableVariant)}
</Typography>
</>
</Grid>
@ -598,7 +598,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// for a section with field names, render the field values. //
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record);
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record, undefined, undefined, tableVariant);
if (section.tier === "T1")
{

View File

@ -748,35 +748,54 @@ input[type="search"]::-webkit-search-results-decoration
padding: 8px 0;
}
.helpContentAlert.success
.helpContentAlert.info,
.banner.info
{
background-color: rgb(234, 242, 255);
color: rgb(20, 51, 102);
}
.helpContentAlert.info .MuiAlert-icon .material-icons-round,
.banner.info .MuiAlert-icon .material-icons-round
{
color: #0062FF;
}
.helpContentAlert.success,
.banner.success
{
background-color: rgb(240, 248, 241);
color: rgb(44, 76, 46);
}
.helpContentAlert.success .MuiAlert-icon .material-icons-round
.helpContentAlert.success .MuiAlert-icon .material-icons-round,
.banner.success .MuiAlert-icon .material-icons-round
{
color: #4CAF50;
}
.helpContentAlert.warning
.helpContentAlert.warning,
.banner.warning
{
background-color: rgb(254, 245, 234);
color: rgb(100, 65, 20);
}
.helpContentAlert.warning .MuiAlert-icon .material-icons-round
.helpContentAlert.warning .MuiAlert-icon .material-icons-round,
.banner.warning .MuiAlert-icon .material-icons-round
{
color: #fb8c00;
}
.helpContentAlert.error
.helpContentAlert.error,
.banner.error
{
background-color: rgb(254, 239, 238);
color: rgb(98, 41, 37);
}
.helpContentAlert.error .MuiAlert-icon .material-icons-round
.helpContentAlert.error .MuiAlert-icon .material-icons-round,
.banner.error .MuiAlert-icon .material-icons-round
{
color: #F44335;
}

View File

@ -24,6 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
@ -70,7 +71,7 @@ export default class DataGridUtils
/*******************************************************************************
**
*******************************************************************************/
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, allowEmptyId = false): GridRowsProp[] =>
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, tableVariant?: QTableVariant, allowEmptyId = false): GridRowsProp[] =>
{
const fields = [...tableMetaData.fields.values()];
const rows = [] as any[];
@ -82,7 +83,7 @@ export default class DataGridUtils
fields.forEach((field) =>
{
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
row[field.name] = ValueUtils.getDisplayValue(field, record, "query", undefined, tableVariant);
});
if (tableMetaData.exposedJoins)
@ -97,7 +98,7 @@ export default class DataGridUtils
fields.forEach((field) =>
{
let fieldName = join.joinTable.name + "." + field.name;
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName);
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName, tableVariant);
});
}
}

View File

@ -23,6 +23,7 @@ import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Ado
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import "datejs"; // https://github.com/datejs/Datejs
import {Chip, ClickAwayListener, Icon} from "@mui/material";
@ -76,14 +77,14 @@ class ValueUtils
** When you have a field, and a record - call this method to get a string or
** element back to display the field's value.
*******************************************************************************/
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string): string | JSX.Element | JSX.Element[]
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string, tableVariant?: QTableVariant): string | JSX.Element | JSX.Element[]
{
const fieldName = overrideFieldName ?? field.name;
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
const rawValue = record.values ? record.values.get(fieldName) : undefined;
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage, tableVariant, record, fieldName);
}
@ -91,14 +92,35 @@ class ValueUtils
** When you have a field and a value (either just a raw value, or a raw and
** display value), call this method to get a string Element to display.
*******************************************************************************/
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view"): string | JSX.Element | JSX.Element[]
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view", tableVariant?: QTableVariant, record?: QRecord, fieldName?: string): string | JSX.Element | JSX.Element[]
{
if (field.hasAdornment(AdornmentType.LINK))
{
const adornment = field.getAdornment(AdornmentType.LINK);
let href = rawValue;
let href = String(rawValue);
let toRecordFromTable = adornment.getValue("toRecordFromTable");
/////////////////////////////////////////////////////////////////////////////////////
// if the link adornment has a 'toRecordFromTableDynamic', then look for a display //
// value named `fieldName`:toRecordFromTableDynamic for the table name. //
/////////////////////////////////////////////////////////////////////////////////////
if(adornment.getValue("toRecordFromTableDynamic"))
{
const toRecordFromTableDynamic = record?.displayValues?.get(fieldName + ":toRecordFromTableDynamic");
if(toRecordFromTableDynamic)
{
toRecordFromTable = toRecordFromTableDynamic;
}
else
{
///////////////////////////////////////////////////////////////////
// if the table name isn't known, then return w/o the adornment. //
///////////////////////////////////////////////////////////////////
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
}
}
const toRecordFromTable = adornment.getValue("toRecordFromTable");
if (toRecordFromTable)
{
if (ValueUtils.getQInstance())
@ -107,7 +129,7 @@ class ValueUtils
if (!tablePath)
{
console.log("Couldn't find path for table: " + toRecordFromTable);
return (displayValue ?? rawValue);
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
}
if (!tablePath.endsWith("/"))
@ -199,12 +221,44 @@ class ValueUtils
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
{
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
let url = rawValue;
if(tableVariant)
{
url += "?tableVariant=" + encodeURIComponent(JSON.stringify(tableVariant));
}
//////////////////////////////////////////////////////////////////////////////
// if the field has the download adornment with a downloadUrlDynamic value, //
// then get the url from a displayValue of `fieldName`:downloadUrlDynamic. //
//////////////////////////////////////////////////////////////////////////////
if(field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
{
const adornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
let downloadUrlDynamicAdornmentValue = adornment.getValue("downloadUrlDynamic");
if(downloadUrlDynamicAdornmentValue)
{
const downloadUrlDynamicValue = record?.displayValues?.get(fieldName + ":downloadUrlDynamic");
if (downloadUrlDynamicValue)
{
url = downloadUrlDynamicValue;
}
else
{
////////////////////////////////////////////////////////////////
// if the url isn't available, then return w/o the adornment. //
////////////////////////////////////////////////////////////////
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
}
}
}
return (<BlobComponent field={field} url={url} filename={displayValue} usage={usage} />);
}
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
}
/*******************************************************************************
** After we know there's no element to be returned (e.g., because no adornment),
** this method does the string formatting.