Compare commits

...

14 Commits

Author SHA1 Message Date
f41b71d3c7 Support combination of non-query table (so goto only) plus variants. 2023-09-29 17:10:06 -05:00
57fefe9671 Merge pull request #33 from Kingsrook/feature/fix-urlencoding-primary-keys
Feature/fix urlencoding primary keys (and some filters)
2023-09-26 10:16:44 -05:00
8a018c34f6 Merge branch 'main' into feature/fix-urlencoding-primary-keys 2023-09-26 08:32:17 -05:00
1b4f70a547 Merge pull request #32 from Kingsrook/feature/CE-609-infrastructure-remove-permissions-from-header
Feature/ce 609 infrastructure remove permissions from header
2023-09-25 16:28:33 -05:00
1f343abbb5 Switch to use jwt_decode library (from auth0) rather than S/O decodeJWT function 2023-09-25 16:27:46 -05:00
37a18bbe0d Add some urlencoding of primary keys in query, view, and run process; updated qqq-frontend-core to also do more urlencoding 2023-09-25 13:33:20 -05:00
98cc2ceb00 Merge remote-tracking branch 'origin/feature/deploy-test-jar' 2023-09-22 10:35:05 -05:00
e351883d73 add check for table 2023-09-22 09:46:00 -05:00
25599d0ca6 Merge pull request #30 from Kingsrook/feature/extensiv-shipped-order-fix
more updates to allow process to be manually ran
2023-09-21 11:43:43 -05:00
01d18902d7 more updates to allow process to be manually ran 2023-09-20 19:52:13 -05:00
7ea50dd7bb Merge branch 'main' into feature/CE-609-infrastructure-remove-permissions-from-header
# Conflicts:
#	package.json
2023-08-17 11:42:51 -05:00
b6b7d8d8b3 CE-609 - Removed DNDTest WIP module 2023-08-15 09:25:45 -05:00
7bf515554d CE-609 - staged-rollout-ready - keeping the auth header, but also setting sessionUUID cookie; placeholder for quick-rollback; added todo#authHeader comments to mark where follow-up needs to happen after happy with new code 2023-08-15 09:08:44 -05:00
7fa42a6eb5 Initial WIP Checkpoint of auth0 userSessions 2023-08-09 09:48:22 -05:00
10 changed files with 227 additions and 90 deletions

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.80", "@kingsrook/qqq-frontend-core": "1.0.82",
"@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",
@ -33,6 +33,7 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"rapidoc": "9.3.4", "rapidoc": "9.3.4",
"react": "18.0.0", "react": "18.0.0",
"react-ace": "10.1.0", "react-ace": "10.1.0",
@ -56,9 +57,7 @@
"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",
"test": "react-scripts test", "test": "react-scripts test"
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -86,8 +85,6 @@
"@types/react-table": "7.7.9", "@types/react-table": "7.7.9",
"@typescript-eslint/eslint-plugin": "5.10.2", "@typescript-eslint/eslint-plugin": "5.10.2",
"@typescript-eslint/parser": "5.10.2", "@typescript-eslint/parser": "5.10.2",
"cypress": "11.0.1",
"cypress-wait-for-stable-dom": "0.1.0",
"eslint": "8.8.0", "eslint": "8.8.0",
"eslint-import-resolver-typescript": "2.5.0", "eslint-import-resolver-typescript": "2.5.0",
"eslint-plugin-import": "2.25.4", "eslint-plugin-import": "2.25.4",

View File

@ -33,7 +33,8 @@ import CssBaseline from "@mui/material/CssBaseline";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {ThemeProvider} from "@mui/material/styles"; import {ThemeProvider} from "@mui/material/styles";
import {LicenseInfo} from "@mui/x-license-pro"; import {LicenseInfo} from "@mui/x-license-pro";
import React, {JSXElementConstructor, Key, ReactElement, useContext, useEffect, useState,} from "react"; import jwt_decode from "jwt-decode";
import React, {JSXElementConstructor, Key, ReactElement, useEffect, useState,} from "react";
import {useCookies} from "react-cookie"; import {useCookies} from "react-cookie";
import {Navigate, Route, Routes, useLocation,} from "react-router-dom"; import {Navigate, Route, Routes, useLocation,} from "react-router-dom";
import {Md5} from "ts-md5/dist/md5"; import {Md5} from "ts-md5/dist/md5";
@ -57,11 +58,11 @@ import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
const qController = Client.getInstance(); const qController = Client.getInstance();
export const SESSION_ID_COOKIE_NAME = "sessionId"; export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
export default function App() export default function App()
{ {
const [, setCookie, removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]); const [, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
const {user, getAccessTokenSilently, logout} = useAuth0(); const {user, getAccessTokenSilently, logout} = useAuth0();
const [loadingToken, setLoadingToken] = useState(false); const [loadingToken, setLoadingToken] = useState(false);
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false); const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
@ -69,8 +70,55 @@ export default function App()
const [branding, setBranding] = useState({} as QBrandingMetaData); const [branding, setBranding] = useState({} as QBrandingMetaData);
const [metaData, setMetaData] = useState({} as QInstance); const [metaData, setMetaData] = useState({} as QInstance);
const [needLicenseKey, setNeedLicenseKey] = useState(true); const [needLicenseKey, setNeedLicenseKey] = useState(true);
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
const [defaultRoute, setDefaultRoute] = useState("/no-apps"); const [defaultRoute, setDefaultRoute] = useState("/no-apps");
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
{
if (!oldToken)
{
return (true);
}
try
{
const oldJSON: any = jwt_decode(oldToken);
const newJSON: any = jwt_decode(newToken);
////////////////////////////////////////////////////////////////////////////////////
// if the old (local storage) token is expired, then we need to store the new one //
////////////////////////////////////////////////////////////////////////////////////
const oldExp = oldJSON["exp"];
if(oldExp * 1000 < (new Date().getTime()))
{
console.log("Access token in local storage was expired.");
return (true);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// remove the exp & iat values from what we compare - as they are always different from auth0 //
// note, this is only deleting them from what we compare, not from what we'd store. //
////////////////////////////////////////////////////////////////////////////////////////////////
delete newJSON["exp"]
delete newJSON["iat"]
delete oldJSON["exp"]
delete oldJSON["iat"]
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
if(different)
{
console.log("Latest access token from auth0 has changed vs localStorage.");
}
return (different);
}
catch(e)
{
console.log("Caught in shouldStoreNewToken: " + e)
}
return (true);
};
useEffect(() => useEffect(() =>
{ {
if (loadingToken) if (loadingToken)
@ -92,20 +140,38 @@ export default function App()
{ {
console.log("Loading token from auth0..."); console.log("Loading token from auth0...");
const accessToken = await getAccessTokenSilently(); const accessToken = await getAccessTokenSilently();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
///////////////////////////////////////////////////////////////////////////////// const lsAccessToken = localStorage.getItem("accessToken");
// we've stopped using session id cook with auth0, so make sure it is not set. // if (shouldStoreNewToken(accessToken, lsAccessToken))
///////////////////////////////////////////////////////////////////////////////// {
removeCookie(SESSION_ID_COOKIE_NAME); console.log("Sending accessToken to backend, requesting a sessionUUID...");
const newSessionUuid = await qController.manageSession(accessToken, null);
setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
localStorage.setItem("accessToken", accessToken);
}
/*
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo#authHeader - this is our quick rollback plan - if we feel the need to stop using the cookie approach. //
// we turn off the shouldStoreNewToken block above, and turn on these 2 lines. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
localStorage.removeItem("accessToken");
*/
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
qController.setGotAuthentication();
qController.setAuthorizationHeaderValue("Bearer " + accessToken);
setLoggedInUser(user);
console.log("Token load complete."); console.log("Token load complete.");
} }
catch (e) catch (e)
{ {
console.log(`Error loading token: ${JSON.stringify(e)}`); console.log(`Error loading token: ${JSON.stringify(e)}`);
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken")
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
logout(); logout();
return; return;
} }
@ -116,9 +182,9 @@ export default function App()
// use a random token if anonymous or mock // // use a random token if anonymous or mock //
///////////////////////////////////////////// /////////////////////////////////////////////
console.log("Generating random token..."); console.log("Generating random token...");
qController.setAuthorizationHeaderValue(null); qController.setAuthorizationHeaderValue(Md5.hashStr(`${new Date()}`));
setIsFullyAuthenticated(true); setIsFullyAuthenticated(true);
setCookie(SESSION_ID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"}); setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
console.log("Token generation complete."); console.log("Token generation complete.");
return; return;
} }
@ -429,11 +495,11 @@ export default function App()
let profileRoutes = {}; let profileRoutes = {};
const gravatarBase = "https://www.gravatar.com/avatar/"; const gravatarBase = "https://www.gravatar.com/avatar/";
const hash = Md5.hashStr(user?.email || "user"); const hash = Md5.hashStr(loggedInUser?.email || "user");
const profilePicture = `${gravatarBase}${hash}`; const profilePicture = `${gravatarBase}${hash}`;
profileRoutes = { profileRoutes = {
type: "collapse", type: "collapse",
name: user?.name, name: loggedInUser?.name ?? "Anonymous",
key: "username", key: "username",
noCollapse: true, noCollapse: true,
icon: <Avatar src={profilePicture} alt="{user?.name}" />, icon: <Avatar src={profilePicture} alt="{user?.name}" />,
@ -495,7 +561,10 @@ export default function App()
{ {
if ((e as QException).status === "401") if ((e as QException).status === "401")
{ {
console.log("Exception is a QException with status = 401. Clearing some of localStorage & cookies");
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
localStorage.removeItem("accessToken")
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
// todo - this is auth0 logout... make more generic // // todo - this is auth0 logout... make more generic //

View File

@ -22,7 +22,7 @@
import {useAuth0} from "@auth0/auth0-react"; import {useAuth0} from "@auth0/auth0-react";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {useCookies} from "react-cookie"; import {useCookies} from "react-cookie";
import {SESSION_ID_COOKIE_NAME} from "App"; import {SESSION_UUID_COOKIE_NAME} from "App";
interface Props interface Props
{ {
@ -33,13 +33,13 @@ interface Props
function HandleAuthorizationError({errorMessage}: Props) function HandleAuthorizationError({errorMessage}: Props)
{ {
const [, , removeCookie] = useCookies([SESSION_ID_COOKIE_NAME]); const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
const {logout} = useAuth0(); const {logout} = useAuth0();
useEffect(() => useEffect(() =>
{ {
logout(); logout();
removeCookie(SESSION_ID_COOKIE_NAME, {path: "/"}); removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
}); });
return ( return (

View File

@ -22,6 +22,7 @@
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
@ -45,8 +46,10 @@ interface Props
isOpen: boolean; isOpen: boolean;
metaData: QInstance; metaData: QInstance;
tableMetaData: QTableMetaData; tableMetaData: QTableMetaData;
tableVariant?: QTableVariant;
closeHandler: () => void; closeHandler: () => void;
mayClose: boolean; mayClose: boolean;
subHeader?: JSX.Element;
} }
GotoRecordDialog.defaultProps = { GotoRecordDialog.defaultProps = {
@ -155,7 +158,9 @@ function GotoRecordDialog(props: Props): JSX.Element
{ {
setError(""); setError("");
const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10); const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, "AND", null, 10);
const queryResult = await qController.query(props.tableMetaData.name, filter) try
{
const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant)
if (queryResult.length == 0) if (queryResult.length == 0)
{ {
setError("Record not found."); setError("Record not found.");
@ -163,7 +168,7 @@ function GotoRecordDialog(props: Props): JSX.Element
} }
else if (queryResult.length == 1) else if (queryResult.length == 1)
{ {
navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${queryResult[0].values.get(props.tableMetaData.primaryKeyField)}`); navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`);
close(); close();
} }
else else
@ -172,6 +177,13 @@ function GotoRecordDialog(props: Props): JSX.Element
setTimeout(() => setError(""), 3000); setTimeout(() => setError(""), 3000);
} }
} }
catch(e)
{
// @ts-ignore
setError(`Error: ${(e && e.message) ? e.message : e}`);
setTimeout(() => setError(""), 6000);
}
}
if(props.tableMetaData) if(props.tableMetaData)
{ {
@ -184,7 +196,9 @@ function GotoRecordDialog(props: Props): JSX.Element
return ( return (
<Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}> <Dialog open={props.isOpen} onClose={() => closeRequested} onKeyPress={(e) => keyPressed(e)} fullWidth maxWidth={"sm"}>
<DialogTitle>Go To...</DialogTitle> <DialogTitle>Go To...</DialogTitle>
<DialogContent> <DialogContent>
{props.subHeader}
{ {
fields.map((field, index) => fields.map((field, index) =>
( (
@ -237,9 +251,11 @@ interface GotoRecordButtonProps
{ {
metaData: QInstance; metaData: QInstance;
tableMetaData: QTableMetaData; tableMetaData: QTableMetaData;
tableVariant?: QTableVariant;
autoOpen?: boolean; autoOpen?: boolean;
buttonVisible?: boolean; buttonVisible?: boolean;
mayClose?: boolean; mayClose?: boolean;
subHeader?: JSX.Element;
} }
GotoRecordButton.defaultProps = { GotoRecordButton.defaultProps = {
@ -268,7 +284,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
{ {
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button> props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} >Go To...</Button>
} }
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} /> <GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -62,10 +62,12 @@ import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDri
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
import ValidationReview from "qqq/components/processes/ValidationReview"; import ValidationReview from "qqq/components/processes/ValidationReview";
import BaseLayout from "qqq/layouts/BaseLayout"; import BaseLayout from "qqq/layouts/BaseLayout";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props interface Props
{ {
process?: QProcessMetaData; process?: QProcessMetaData;
@ -74,7 +76,7 @@ interface Props
isModal?: boolean; isModal?: boolean;
isWidget?: boolean; isWidget?: boolean;
isReport?: boolean; isReport?: boolean;
recordIds?: string | QQueryFilter; recordIds?: string[] | QQueryFilter;
closeModalHandler?: (event: object, reason: string) => void; closeModalHandler?: (event: object, reason: string) => void;
forceReInit?: number; forceReInit?: number;
overrideLabel?: string; overrideLabel?: string;
@ -88,6 +90,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
const processNameParam = useParams().processName; const processNameParam = useParams().processName;
const processName = process === null ? processNameParam : process.name; const processName = process === null ? processNameParam : process.name;
let tableVariantLocalStorageKey: string | null = null;
if(table)
{
tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${table.name}`;
}
/////////////////// ///////////////////
// process state // // process state //
@ -221,12 +228,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const download = (url: string, fileName: string) => const download = (url: string, fileName: string) =>
{ {
const qController = Client.getInstance(); /////////////////////////////////////////////////////////////////////////////////////////////
// todo - this could be simplified. //
// it was originally built like this when we had to submit full access token to backend... //
/////////////////////////////////////////////////////////////////////////////////////////////
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open("POST", url); xhr.open("POST", url);
xhr.responseType = "blob"; xhr.responseType = "blob";
let formData = new FormData(); let formData = new FormData();
////////////////////////////////////
// todo#authHeader - delete this. //
////////////////////////////////////
const qController = Client.getInstance();
formData.append("Authorization", qController.getAuthorizationHeaderValue()); formData.append("Authorization", qController.getAuthorizationHeaderValue());
// @ts-ignore // @ts-ignore
@ -376,7 +390,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{ {
if (processValues[key]) if (processValues[key])
{ {
formFields[key].possibleValueProps.initialDisplayValue = processValues[key] 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>();
@ -385,7 +399,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
formFields[key].possibleValueProps.otherValues.set(otherKey, processValues[otherKey]); formFields[key].possibleValueProps.otherValues.set(otherKey, processValues[otherKey]);
}); });
} }
}) });
} }
return ( return (
@ -800,9 +814,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
Object.keys(initialValues).forEach((ivKey: any) => Object.keys(initialValues).forEach((ivKey: any) =>
{ {
dynamicFormFields[key].possibleValueProps.otherValues.set(ivKey, initialValues[ivKey]); dynamicFormFields[key].possibleValueProps.otherValues.set(ivKey, initialValues[ivKey]);
}) });
} }
}) });
//////////////////////////////////////////////////// ////////////////////////////////////////////////////
// disable all fields if this is a bulk edit form // // disable all fields if this is a bulk edit form //
@ -1075,8 +1089,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
let queryStringPairsForInit = []; let queryStringPairsForInit = [];
if (urlSearchParams.get("recordIds")) if (urlSearchParams.get("recordIds"))
{ {
const recordIdsFromQueryString = urlSearchParams.get("recordIds").split(",");
const encodedRecordIds = recordIdsFromQueryString.map(r => encodeURIComponent(r)).join(",");
queryStringPairsForInit.push("recordsParam=recordIds"); queryStringPairsForInit.push("recordsParam=recordIds");
queryStringPairsForInit.push(`recordIds=${urlSearchParams.get("recordIds")}`); queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
} }
else if (urlSearchParams.get("filterJSON")) else if (urlSearchParams.get("filterJSON"))
{ {
@ -1090,16 +1106,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// } // }
else if (recordIds) else if (recordIds)
{ {
if (typeof recordIds === "string") if (recordIds instanceof QQueryFilter)
{
queryStringPairsForInit.push("recordsParam=recordIds");
queryStringPairsForInit.push(`recordIds=${recordIds}`);
}
else if (recordIds instanceof QQueryFilter)
{ {
queryStringPairsForInit.push("recordsParam=filterJSON"); queryStringPairsForInit.push("recordsParam=filterJSON");
queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`); queryStringPairsForInit.push(`filterJSON=${JSON.stringify(recordIds)}`);
} }
else if (typeof recordIds === "object" && recordIds.length)
{
const encodedRecordIds = recordIds.map(r => encodeURIComponent(r)).join(",");
queryStringPairsForInit.push("recordsParam=recordIds");
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
}
}
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
{
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
queryStringPairsForInit.push(`tableVariant=${JSON.stringify(tableVariant)}`);
} }
try try
@ -1149,7 +1172,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (tableMetaData) if (tableMetaData)
{ {
queryStringPairsForInit.push(`tableName=${tableMetaData.name}`) queryStringPairsForInit.push(`tableName=${encodeURIComponent(tableMetaData.name)}`);
} }
try try
@ -1187,6 +1210,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
formData.append(key, values[key]); formData.append(key, values[key]);
}); });
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
{
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
formData.append("tableVariant", JSON.stringify(tableVariant));
}
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)) if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
{ {
const bulkEditEnabledFields: string[] = []; const bulkEditEnabledFields: string[] = [];

View File

@ -82,7 +82,8 @@ const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns"; const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables"; const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density"; const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
interface Props interface Props
{ {
@ -233,7 +234,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter); const [recordIdsForProcess, setRecordIdsForProcess] = useState([] as string[] | QQueryFilter);
const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string); const [columnStatsFieldName, setColumnStatsFieldName] = useState(null as string);
const [columnStatsField, setColumnStatsField] = useState(null as QFieldMetaData); const [columnStatsField, setColumnStatsField] = useState(null as QFieldMetaData);
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string) const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
@ -538,15 +539,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<CustomWidthTooltip title={tooltipHTML}> <CustomWidthTooltip title={tooltipHTML}>
<IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton> <IconButton sx={{p: 0, fontSize: "0.5rem", mb: 1, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">emergency</Icon></IconButton>
</CustomWidthTooltip> </CustomWidthTooltip>
{ {tableVariant && getTableVariantHeader()}
tableVariant &&
<Typography variant="h6" color="text" fontWeight="light">
{tableMetaData.variantTableLabel}: {tableVariant.name}
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
</Tooltip>
</Typography>
}
</div>); </div>);
} }
else else
@ -554,19 +547,23 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return ( return (
<div> <div>
{label} {label}
{ {tableVariant && getTableVariantHeader()}
tableVariant &&
<Typography variant="h6" color="text" fontWeight="light">
{tableMetaData.variantTableLabel}: {tableVariant.name}
<Tooltip title={`Change ${tableMetaData.variantTableLabel}`}>
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
</Tooltip>
</Typography>
}
</div>); </div>);
} }
}; };
const getTableVariantHeader = () =>
{
return (
<Typography variant="h6" color="text" fontWeight="light">
{tableMetaData?.variantTableLabel}: {tableVariant?.name}
<Tooltip title={`Change ${tableMetaData?.variantTableLabel}`}>
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
</Tooltip>
</Typography>
);
}
const updateTable = () => const updateTable = () =>
{ {
setLoading(true); setLoading(true);
@ -925,11 +922,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
if (table.primaryKeyField !== "id") if (table.primaryKeyField !== "id")
{ {
navigate(`${metaData.getTablePathByName(tableName)}/${params.row[tableMetaData.primaryKeyField]}`); navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.row[tableMetaData.primaryKeyField])}`);
} }
else else
{ {
navigate(`${metaData.getTablePathByName(tableName)}/${params.id}`); navigate(`${metaData.getTablePathByName(tableName)}/${encodeURIComponent(params.id)}`);
} }
}, 100); }, 100);
} }
@ -1139,6 +1136,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<body> <body>
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}... Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
<form id="exportForm" method="post" action="${url}" > <form id="exportForm" method="post" action="${url}" >
<!-- todo#authHeader - remove this. -->
<input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}"> <input type="hidden" name="Authorization" value="${qController.getAuthorizationHeaderValue()}">
<input type="hidden" name="fields" value="${visibleFields.join(",")}"> <input type="hidden" name="fields" value="${visibleFields.join(",")}">
<input type="hidden" name="filter" id="filter"> <input type="hidden" name="filter" id="filter">
@ -1188,17 +1186,17 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
if (selectFullFilterState === "filter") if (selectFullFilterState === "filter")
{ {
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel))}`; return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}`;
} }
if (selectFullFilterState === "filterSubset") if (selectFullFilterState === "filterSubset")
{ {
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(tableMetaData, filterModel, selectionSubsetSize))}`; return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel, selectionSubsetSize)))}`;
} }
if (selectedIds.length > 0) if (selectedIds.length > 0)
{ {
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`; return `?recordsParam=recordIds&recordIds=${selectedIds.map(r => encodeURIComponent(r)).join(",")}`;
} }
return ""; return "";
@ -1216,11 +1214,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
} }
else if (selectedIds.length > 0) else if (selectedIds.length > 0)
{ {
setRecordIdsForProcess(selectedIds.join(",")); setRecordIdsForProcess(selectedIds);
} }
else else
{ {
setRecordIdsForProcess(""); setRecordIdsForProcess([]);
} }
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`); navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`);
@ -1886,10 +1884,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// instead, try to just render a Goto Record button, in auto-open, and may-not-close modes // // instead, try to just render a Goto Record button, in auto-open, and may-not-close modes //
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET)) if (tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_QUERY) && tableMetaData.capabilities.has(Capability.TABLE_GET))
{
if(tableMetaData?.usesVariants && (!tableVariant || tableVariantPromptOpen))
{ {
return ( return (
<BaseLayout> <BaseLayout>
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} autoOpen={true} buttonVisible={false} mayClose={false} /> <TableVariantDialog table={tableMetaData} isOpen={true} closeHandler={(value: QTableVariant) =>
{
setTableVariantPromptOpen(false);
setTableVariant(value);
}} />
</BaseLayout>
);
}
return (
<BaseLayout>
<GotoRecordButton metaData={metaData} tableMetaData={tableMetaData} tableVariant={tableVariant} autoOpen={true} buttonVisible={false} mayClose={false} subHeader={
<Box mb={2}>
{getTableVariantHeader()}
</Box>
} />
</BaseLayout> </BaseLayout>
); );
} }

View File

@ -931,7 +931,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
activeModalProcess && activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}> <Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<div className="modalProcess"> <div className="modalProcess">
<ProcessRun process={activeModalProcess} isModal={true} table={tableMetaData} recordIds={id} closeModalHandler={closeModalProcess} /> <ProcessRun process={activeModalProcess} isModal={true} table={tableMetaData} recordIds={[id]} closeModalHandler={closeModalProcess} />
</div> </div>
</Modal> </Modal>
} }

View File

@ -246,7 +246,7 @@ export default class DataGridUtils
if (key === tableMetaData.primaryKeyField && linkBase) if (key === tableMetaData.primaryKeyField && linkBase)
{ {
column.renderCell = (cellValues: any) => ( column.renderCell = (cellValues: any) => (
<Link to={`${linkBase}${cellValues.value}`} onClick={(e) => e.stopPropagation()}>{cellValues.value}</Link> <Link to={`${linkBase}${encodeURIComponent(cellValues.value)}`} onClick={(e) => e.stopPropagation()}>{cellValues.value}</Link>
); );
} }
}); });

View File

@ -95,6 +95,11 @@ export default class HtmlUtils
form.setAttribute("target", "downloadIframe"); form.setAttribute("target", "downloadIframe");
iframe.appendChild(form); iframe.appendChild(form);
/////////////////////////////////////////////////////////////////////////////////////////////
// todo#authHeader - remove after comfortable with sessionUUID //
// todo - this could be simplified (i think?) //
// it was originally built like this when we had to submit full access token to backend... //
/////////////////////////////////////////////////////////////////////////////////////////////
const authorizationInput = document.createElement("input"); const authorizationInput = document.createElement("input");
authorizationInput.setAttribute("type", "hidden"); authorizationInput.setAttribute("type", "hidden");
authorizationInput.setAttribute("id", "authorizationInput"); authorizationInput.setAttribute("id", "authorizationInput");
@ -118,6 +123,11 @@ export default class HtmlUtils
{ {
if(url.startsWith("data:")) if(url.startsWith("data:"))
{ {
/////////////////////////////////////////////////////////////////////////////////////////////
// todo#authHeader - remove the Authorization input after comfortable with sessionUUID //
// todo - this could be simplified (i think?) //
// it was originally built like this when we had to submit full access token to backend... //
/////////////////////////////////////////////////////////////////////////////////////////////
const openInWindow = window.open("", "_blank"); const openInWindow = window.open("", "_blank");
openInWindow.document.write(`<html lang="en"> openInWindow.document.write(`<html lang="en">
<body style="margin: 0"> <body style="margin: 0">

View File

@ -49,6 +49,7 @@ module.exports = function (app)
app.use("/data/*", getRequestHandler()); app.use("/data/*", getRequestHandler());
app.use("/widget/*", getRequestHandler()); app.use("/widget/*", getRequestHandler());
app.use("/serverInfo", getRequestHandler()); app.use("/serverInfo", getRequestHandler());
app.use("/manageSession", getRequestHandler());
app.use("/processes", getRequestHandler()); app.use("/processes", getRequestHandler());
app.use("/reports", getRequestHandler()); app.use("/reports", getRequestHandler());
app.use("/images", getRequestHandler()); app.use("/images", getRequestHandler());