From ebafc3e97ebbef0b2defe0ee2b1ae30d00427fbf Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 9 May 2023 13:02:54 -0500 Subject: [PATCH 01/14] Updates to work with branch-specific maven deployments in/with circleci --- .circleci/adjust-pom-version.sh | 23 +++++++++++++++++++++++ .circleci/config.yml | 4 ++++ 2 files changed, 27 insertions(+) create mode 100755 .circleci/adjust-pom-version.sh diff --git a/.circleci/adjust-pom-version.sh b/.circleci/adjust-pom-version.sh new file mode 100755 index 0000000..054fbdf --- /dev/null +++ b/.circleci/adjust-pom-version.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then + echo "Error: env vars CIRCLE_BRANCH and CIRCLE_TAG were not set." + exit 1; +fi + +if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ]; then + echo "On a primary branch [$CIRCLE_BRANCH] - will not edit the pom version."; + exit 0; +fi + +if [ -n "$CIRCLE_BRANCH" ]; then + SLUG=$(echo $CIRCLE_BRANCH | sed 's/[^a-zA-Z0-9]/-/g') +else + SLUG=$(echo $CIRCLE_TAG | sed 's/^snapshot-//g') +fi + +POM=$(dirname $0)/../pom.xml + +echo "Updating $POM to: $SLUG-SNAPSHOT" +sed -i "s/.*/$SLUG-SNAPSHOT<\/revision>/" $POM +git diff $POM diff --git a/.circleci/config.yml b/.circleci/config.yml index f6d9483..99c4892 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -71,6 +71,10 @@ commands: mvn_deploy: steps: - checkout + - run: + name: Adjust pom version + command: | + .circleci/adjust-pom-version.sh - restore_cache: keys: - v1-dependencies-{{ checksum "pom.xml" }} From bc4181908ac716b465419abe6c56ff2da98164bd Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Wed, 10 May 2023 15:43:09 -0500 Subject: [PATCH 02/14] CTLE-433: fixed reveal adornment style --- src/qqq/utils/qqq/ValueUtils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index 655c22f..ca8d1b9 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -521,7 +521,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s { displayValue && ( adornmentFieldsMap.get(fieldName) === true ? ( - + handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_on {displayValue} @@ -540,7 +540,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s ):( - handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} + handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} ) ) } From 306640e0ee8f9afaecc4510f5192c6a60ae570fc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 10 May 2023 17:08:37 -0500 Subject: [PATCH 03/14] Handle warnings on delete; move more between-page state to state object (out of query string); --- package.json | 2 +- src/qqq/components/forms/EntityForm.tsx | 25 ++++---- src/qqq/pages/records/query/RecordQuery.tsx | 42 ++++++++----- src/qqq/pages/records/view/RecordView.tsx | 65 +++++++++++++++++---- src/qqq/utils/HtmlUtils.ts | 42 +++++++++++++ 5 files changed, 138 insertions(+), 38 deletions(-) create mode 100644 src/qqq/utils/HtmlUtils.ts diff --git a/package.json b/package.json index a258814..7b9f81a 100644 --- a/package.json +++ b/package.json @@ -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.61", + "@kingsrook/qqq-frontend-core": "1.0.62", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 471735c..6e1a748 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -41,6 +41,7 @@ import QDynamicForm from "qqq/components/forms/DynamicForm"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import MDTypography from "qqq/components/legacy/MDTypography"; import QRecordSidebar from "qqq/components/misc/RecordSidebar"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; @@ -82,7 +83,6 @@ function EntityForm(props: Props): JSX.Element const [warningContent, setWarningContent] = useState(""); const [asyncLoadInited, setAsyncLoadInited] = useState(false); - const [formValues, setFormValues] = useState({} as { [key: string]: string }); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [record, setRecord] = useState(null as QRecord); const [tableSections, setTableSections] = useState(null as QTableSection[]); @@ -185,8 +185,6 @@ function EntityForm(props: Props): JSX.Element initialValues[key] = record.values.get(key); }); - //? safe to delete? setFormValues(formValues); - if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE)) { setNotAllowedError("Records may not be edited in this table"); @@ -416,8 +414,8 @@ function EntityForm(props: Props): JSX.Element } else { - const path = `${location.pathname.replace(/\/edit$/, "")}?updateSuccess=true`; - navigate(path); + const path = location.pathname.replace(/\/edit$/, ""); + navigate(path, {state: {updateSuccess: true}}); } }) .catch((error) => @@ -427,12 +425,13 @@ function EntityForm(props: Props): JSX.Element if(error.message.toLowerCase().startsWith("warning")) { - const path = `${location.pathname.replace(/\/edit$/, "")}?updateSuccess=true&warning=${encodeURIComponent(error.message)}`; - navigate(path); + const path = location.pathname.replace(/\/edit$/, ""); + navigate(path, {state: {updateSuccess: true, warning: error.message}}); } else { setAlertContent(error.message); + HtmlUtils.autoScroll(0); } }); } @@ -448,20 +447,22 @@ function EntityForm(props: Props): JSX.Element } else { - const path = `${location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField))}?createSuccess=true`; - navigate(path); + const path = location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField)); + navigate(path, {state: {createSuccess: true}}); } }) .catch((error) => { if(error.message.toLowerCase().startsWith("warning")) { - const path = `${location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField))}?createSuccess=true&warning=${encodeURIComponent(error.message)}`; + const path = location.pathname.replace(/create$/, record.values.get(tableMetaData.primaryKeyField)); navigate(path); + navigate(path, {state: {createSuccess: true, warning: error.message}}); } else { setAlertContent(error.message); + HtmlUtils.autoScroll(0); } }); } @@ -499,12 +500,12 @@ function EntityForm(props: Props): JSX.Element {alertContent ? ( - {alertContent} + setAlertContent(null)}>{alertContent} ) : ("")} {warningContent ? ( - {warningContent} + setWarningContent(null)}>{warningContent} ) : ("")} diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index a28910b..4af7be4 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -97,12 +97,31 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const tableName = table.name; const [searchParams] = useSearchParams(); - const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(searchParams.has("deleteSuccess")); + const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(false); + const [warningAlert, setWarningAlert] = useState(null as string); const [successAlert, setSuccessAlert] = useState(null as string); const location = useLocation(); const navigate = useNavigate(); + if(location.state) + { + let state: any = location.state; + if(state["deleteSuccess"]) + { + setShowSuccessfullyDeletedAlert(true); + delete state["deleteSuccess"]; + } + + if(state["warning"]) + { + setWarningAlert(state["warning"]); + delete state["warning"]; + } + + window.history.replaceState(state, ""); + } + const pathParts = location.pathname.replace(/\/+$/, "").split("/"); //////////////////////////////////////////// @@ -1815,23 +1834,20 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element )} { (tableLabel && showSuccessfullyDeletedAlert) ? ( - - { - setShowSuccessfullyDeletedAlert(false); - }}> - {`${tableLabel} successfully deleted`} - + setShowSuccessfullyDeletedAlert(false)}>{`${tableLabel} successfully deleted`} ) : null } { (successAlert) ? ( - - { - setSuccessAlert(null); - }}> - {successAlert} - + setSuccessAlert(null)}>{successAlert} + + ) : null + } + { + (warningAlert) ? ( + + setWarningAlert(null)}>{warningAlert} ) : null } diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index aac96eb..546648b 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -43,7 +43,7 @@ import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Modal from "@mui/material/Modal"; import React, {useContext, useEffect, useState} from "react"; -import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom"; +import {useLocation, useNavigate, useParams} from "react-router-dom"; import QContext from "QContext"; import AuditBody from "qqq/components/audits/AuditBody"; import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons"; @@ -53,6 +53,7 @@ import DashboardWidgets from "qqq/components/widgets/DashboardWidgets"; import BaseLayout from "qqq/layouts/BaseLayout"; import ProcessRun from "qqq/pages/processes/ProcessRun"; import HistoryUtils from "qqq/utils/HistoryUtils"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; import TableUtils from "qqq/utils/qqq/TableUtils"; @@ -97,10 +98,10 @@ function RecordView({table, launchProcess}: Props): JSX.Element const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [actionsMenu, setActionsMenu] = useState(null); - const [notFoundMessage, setNotFoundMessage] = useState(null); + const [notFoundMessage, setNotFoundMessage] = useState(null as string); + const [errorMessage, setErrorMessage] = useState(null as string) const [successMessage, setSuccessMessage] = useState(null as string); const [warningMessage, setWarningMessage] = useState(null as string); - const [searchParams] = useSearchParams(); const {setPageHeader} = useContext(QContext); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [reloadCounter, setReloadCounter] = useState(0); @@ -116,6 +117,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element { setSuccessMessage(null); setNotFoundMessage(null); + setErrorMessage(null); setAsyncLoadInited(false); setTableMetaData(null); setRecord(null); @@ -423,14 +425,26 @@ function RecordView({table, launchProcess}: Props): JSX.Element setSectionFieldElements(sectionFieldElements); setNonT1TableSections(nonT1TableSections); - if (searchParams.get("createSuccess") || searchParams.get("updateSuccess")) + if(location.state) { - setSuccessMessage(`${tableMetaData.label} successfully ${searchParams.get("createSuccess") ? "created" : "updated"}`); - } - if (searchParams.get("warning")) - { - setWarningMessage(searchParams.get("warning")); + let state: any = location.state; + if (state["createSuccess"] || state["updateSuccess"]) + { + setSuccessMessage(`${tableMetaData.label} successfully ${state["createSuccess"] ? "created" : "updated"}`); + } + + if (state["warning"]) + { + setWarningMessage(state["warning"]); + } + + delete state["createSuccess"] + delete state["updateSuccess"] + delete state["warning"] + + window.history.replaceState(state, ""); } + })(); } @@ -452,8 +466,25 @@ function RecordView({table, launchProcess}: Props): JSX.Element await qController.delete(tableName, id) .then(() => { - const path = `${pathParts.slice(0, -1).join("/")}?deleteSuccess=true`; - navigate(path); + const path = pathParts.slice(0, -1).join("/"); + navigate(path, {state: {deleteSuccess: true}}); + }) + .catch((error) => + { + setDeleteConfirmationOpen(false); + console.log("Caught:"); + console.log(error); + + if(error.message.toLowerCase().startsWith("warning")) + { + const path = pathParts.slice(0, -1).join("/"); + navigate(path, {state: {deleteSuccess: true, warning: error.message}}); + } + else + { + setErrorMessage(error.message); + HtmlUtils.autoScroll(0); + } }); })(); }; @@ -648,7 +679,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element { successMessage ? - + { setSuccessMessage(null); }}> @@ -666,6 +697,16 @@ function RecordView({table, launchProcess}: Props): JSX.Element : ("") } + { + errorMessage ? + + { + setErrorMessage(null); + }}> + {errorMessage} + + : ("") + } diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts new file mode 100644 index 0000000..8b4887e --- /dev/null +++ b/src/qqq/utils/HtmlUtils.ts @@ -0,0 +1,42 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +export default class HtmlUtils +{ + + /******************************************************************************* + ** Since our pages are set (w/ style on the HTML element) to smooth scroll, + ** if you ever want to do an "auto" scroll (e.g., instant, not smooth), you can + ** call this method, which will remove that style, and then put it back. + *******************************************************************************/ + static autoScroll = (top: number, left: number = 0) => + { + let htmlElement = document.querySelector("html"); + const initialScrollBehavior = htmlElement.style.scrollBehavior; + htmlElement.style.scrollBehavior = "auto"; + setTimeout(() => + { + window.scrollTo({top: top, left: left, behavior: "auto"}); + htmlElement.style.scrollBehavior = initialScrollBehavior; + }); + }; + +} \ No newline at end of file From ff9ebeea1b04f359db464e1ab7471515fda32137 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 10 May 2023 17:08:52 -0500 Subject: [PATCH 04/14] working bulk-edit test --- .../materialdashboard/tests/BulkEditTest.java | 114 ++++++++++++++++++ .../processes/person.bulkEdit/step/edit.json | 34 ++++-- .../person.bulkEdit/step/review-result.json | 60 +++++++++ .../person.bulkEdit/step/review.json | 40 ++++++ 4 files changed, 237 insertions(+), 11 deletions(-) create mode 100755 src/test/java/com/kingsrook/qqq/materialdashboard/tests/BulkEditTest.java create mode 100644 src/test/resources/fixtures/processes/person.bulkEdit/step/review-result.json create mode 100644 src/test/resources/fixtures/processes/person.bulkEdit/step/review.json diff --git a/src/test/java/com/kingsrook/qqq/materialdashboard/tests/BulkEditTest.java b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/BulkEditTest.java new file mode 100755 index 0000000..54ec041 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/materialdashboard/tests/BulkEditTest.java @@ -0,0 +1,114 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.materialdashboard.tests; + + +import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest; +import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Test for the scripts table + *******************************************************************************/ +public class BulkEditTest extends QBaseSeleniumTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin) + { + super.addJavalinRoutes(qSeleniumJavalin); + addCommonRoutesForThisTest(qSeleniumJavalin); + qSeleniumJavalin + .withRouteToFile("/metaData/process/person.bulkEdit", "metaData/process/person.bulkEdit.json") + .withRouteToFile("/processes/person.bulkEdit/init", "/processes/person.bulkEdit/init.json") + .withRouteToFile("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/step/edit", "/processes/person.bulkEdit/step/edit.json") + .withRouteToFile("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/step/review", "/processes/person.bulkEdit/step/review.json") + ; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void addCommonRoutesForThisTest(QSeleniumJavalin qSeleniumJavalin) + { + qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json"); + qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + // @RepeatedTest(100) + void test() + { + qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person"); + qSeleniumLib.waitForSelectorContaining("button", "selection").click(); + qSeleniumLib.waitForSelectorContaining("li", "This page").click(); + qSeleniumLib.waitForSelectorContaining("div", "records on this page are selected"); + + qSeleniumLib.waitForSelectorContaining("button", "action").click(); + qSeleniumLib.waitForSelectorContaining("li", "bulk edit").click(); + + ///////////////// + // edit screen // + ///////////////// + qSeleniumLib.waitForSelector("#bulkEditSwitch-firstName").click(); + qSeleniumLib.waitForSelector("input[name=firstName]").click(); + qSeleniumLib.waitForSelector("input[name=firstName]").sendKeys("John"); + qSeleniumLib.waitForSelectorContaining("button", "next").click(); + + /////////////////////// + // validation screen // + /////////////////////// + qSeleniumLib.waitForSelectorContaining("span", "How would you like to proceed").click(); + qSeleniumLib.waitForSelectorContaining("button", "next").click(); + + ////////////////////////////////////////////////////////////// + // need to change the result of the 'review' step this time // + ////////////////////////////////////////////////////////////// + qSeleniumLib.waitForSelectorContaining("div", "Person Bulk Edit: Review").click(); + qSeleniumJavalin.clearRoutes(); + qSeleniumJavalin.stop(); + addCommonRoutesForThisTest(qSeleniumJavalin); + qSeleniumJavalin.withRouteToFile("/processes/person.bulkEdit/74a03a7d-2f53-4784-9911-3a21f7646c43/step/review", "/processes/person.bulkEdit/step/review-result.json"); + qSeleniumJavalin.restart(); + qSeleniumLib.waitForSelectorContaining("button", "submit").click(); + + /////////////////// + // result screen // + /////////////////// + qSeleniumLib.waitForSelectorContaining("div", "Person Bulk Edit: Result").click(); + qSeleniumLib.waitForSelectorContaining("button", "close").click(); + + // qSeleniumLib.waitForever(); + } + +} diff --git a/src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json b/src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json index b3e0fde..40a417b 100644 --- a/src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json +++ b/src/test/resources/fixtures/processes/person.bulkEdit/step/edit.json @@ -1,12 +1,24 @@ { - "values": { - "firstName": "Kahhhhn", - "valuesBeingUpdated": "First Name will be set to: Kahhhhn", - "bulkEditEnabledFields": "firstName", - "recordsParam": "recordIds", - "recordIds": "1,2,3,4,5", - "queryFilterJSON": "{\"criteria\":[{\"fieldName\":\"id\",\"operator\":\"IN\",\"values\":[\"1\",\"2\",\"3\",\"4\",\"5\"]}]}" - }, - "processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43", - "nextStep": "review" -} + "values": { + "transactionLevel": "process", + "tableName": "person", + "recordsParam": "recordIds", + "supportsFullValidation": true, + "recordIds": "1,2,3,4,5", + "sourceTable": "person", + "extract": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep", + "codeType": "JAVA" + }, + "recordCount": 5, + "previewMessage": "This is a preview of the records that will be updated.", + "transform": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep", + "codeType": "JAVA" + }, + "destinationTable": "person", + "bulkEditEnabledFields": "firstName" + }, + "processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43", + "nextStep": "review" +} \ No newline at end of file diff --git a/src/test/resources/fixtures/processes/person.bulkEdit/step/review-result.json b/src/test/resources/fixtures/processes/person.bulkEdit/step/review-result.json new file mode 100644 index 0000000..c7b2417 --- /dev/null +++ b/src/test/resources/fixtures/processes/person.bulkEdit/step/review-result.json @@ -0,0 +1,60 @@ +{ + "values": { + "transactionLevel": "process", + "tableName": "person", + "recordsParam": "recordIds", + "supportsFullValidation": true, + "doFullValidation": true, + "recordIds": "1,2,3,4,5", + "sourceTable": "person", + "extract": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep", + "codeType": "JAVA" + }, + "validationSummary": [ + { + "status": "OK", + "count": 5, + "message": "Person records will be edited.", + "singularFutureMessage": "Person record will be edited.", + "pluralFutureMessage": "Person records will be edited.", + "singularPastMessage": "Person record was edited.", + "pluralPastMessage": "Person records were edited." + }, + { + "status": "INFO", + "message": "First name will be set to John" + } + ], + "recordCount": 5, + "previewMessage": "This is a preview of the records that will be updated.", + "transform": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep", + "codeType": "JAVA" + }, + "destinationTable": "person", + "bulkEditEnabledFields": "firstName", + "processResults": [ + { + "status": "OK", + "count": 5, + "message": "Person records were edited.", + "singularFutureMessage": "Person record will be edited.", + "pluralFutureMessage": "Person records will be edited.", + "singularPastMessage": "Person record was edited.", + "pluralPastMessage": "Person records were edited." + }, + { + "status": "INFO", + "message": "Mapping Exception Type was cleared out" + } + ], + "load": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep", + "codeType": "JAVA" + }, + "basepullReadyToUpdateTimestamp": true + }, + "processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43", + "nextStep": "result" +} \ No newline at end of file diff --git a/src/test/resources/fixtures/processes/person.bulkEdit/step/review.json b/src/test/resources/fixtures/processes/person.bulkEdit/step/review.json new file mode 100644 index 0000000..3f34c35 --- /dev/null +++ b/src/test/resources/fixtures/processes/person.bulkEdit/step/review.json @@ -0,0 +1,40 @@ +{ + "values": { + "transactionLevel": "process", + "tableName": "person", + "recordsParam": "recordIds", + "supportsFullValidation": true, + "doFullValidation": true, + "recordIds": "1,2,3,4,5", + "sourceTable": "person", + "extract": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep", + "codeType": "JAVA" + }, + "validationSummary": [ + { + "status": "OK", + "count": 5, + "message": "Person records will be edited.", + "singularFutureMessage": "Person record will be edited.", + "pluralFutureMessage": "Person records will be edited.", + "singularPastMessage": "Person record was edited.", + "pluralPastMessage": "Person records were edited." + }, + { + "status": "INFO", + "message": "First name will be set to John" + } + ], + "recordCount": 5, + "previewMessage": "This is a preview of the records that will be updated.", + "transform": { + "name": "com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep", + "codeType": "JAVA" + }, + "destinationTable": "person", + "bulkEditEnabledFields": "firstName" + }, + "processUUID": "74a03a7d-2f53-4784-9911-3a21f7646c43", + "nextStep": "review" +} \ No newline at end of file From 084ed0732dcbbd12366e21144c9457f7b3a28455 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 24 May 2023 17:40:30 -0500 Subject: [PATCH 05/14] Support for BLOB, file downloads --- package.json | 2 +- src/qqq/utils/DataGridUtils.tsx | 25 +++++++++++- src/qqq/utils/HtmlUtils.ts | 70 ++++++++++++++++++++++++++++++++ src/qqq/utils/qqq/ValueUtils.tsx | 63 ++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 65b58a2..a3935cc 100644 --- a/package.json +++ b/package.json @@ -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.66", + "@kingsrook/qqq-frontend-core": "1.0.67", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index f43364f..98c57ef 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -168,6 +168,23 @@ export default class DataGridUtils sortedKeys.forEach((key) => { const field = tableMetaData.fields.get(key); + if(field.isHeavy) + { + if(field.type == QFieldType.BLOB) + { + //////////////////////////////////////////////////////// + // assume we DO want heavy blobs - as download links. // + //////////////////////////////////////////////////////// + } + else + { + /////////////////////////////////////////////////// + // otherwise, skip heavy fields on query screen. // + /////////////////////////////////////////////////// + return; + } + } + const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix); if(key === tableMetaData.primaryKeyField && linkBase && namePrefix == null) @@ -244,6 +261,7 @@ export default class DataGridUtils const widths: Map = new Map([ ["small", 100], ["medium", 200], + ["medlarge", 300], ["large", 400], ["xlarge", 600] ]); @@ -260,7 +278,7 @@ export default class DataGridUtils let headerName = labelPrefix ? labelPrefix + field.label : field.label; let fieldName = namePrefix ? namePrefix + field.name : field.name; - const column = { + const column: GridColDef = { field: fieldName, type: columnType, headerName: headerName, @@ -269,6 +287,11 @@ export default class DataGridUtils filterOperators: filterOperators, }; + if(field.type == QFieldType.BLOB) + { + column.filterable = false; + } + column.renderCell = (cellValues: any) => ( (cellValues.value) ); diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts index 5197867..1bf0712 100644 --- a/src/qqq/utils/HtmlUtils.ts +++ b/src/qqq/utils/HtmlUtils.ts @@ -19,6 +19,8 @@ * along with this program. If not, see . */ +import Client from "qqq/utils/qqq/Client"; + /******************************************************************************* ** Utility functions for basic html/webpage/browser things. *******************************************************************************/ @@ -59,4 +61,72 @@ export default class HtmlUtils document.body.removeChild(element); }; + /******************************************************************************* + ** Download a server-side generated file. + *******************************************************************************/ + static downloadUrlViaIFrame = (url: string) => + { + if (document.getElementById("downloadIframe")) + { + document.body.removeChild(document.getElementById("downloadIframe")); + } + + const iframe = document.createElement("iframe"); + iframe.setAttribute("id", "downloadIframe"); + iframe.setAttribute("name", "downloadIframe"); + iframe.style.display = "none"; + // todo - onload event handler to let us know when done? + document.body.appendChild(iframe); + + const form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", url); + form.setAttribute("target", "downloadIframe"); + iframe.appendChild(form); + + const authorizationInput = document.createElement("input"); + authorizationInput.setAttribute("type", "hidden"); + authorizationInput.setAttribute("id", "authorizationInput"); + authorizationInput.setAttribute("name", "Authorization"); + authorizationInput.setAttribute("value", Client.getInstance().getAuthorizationHeaderValue()); + form.appendChild(authorizationInput); + + const downloadInput = document.createElement("input"); + downloadInput.setAttribute("type", "hidden"); + downloadInput.setAttribute("name", "download"); + downloadInput.setAttribute("value", "1"); + form.appendChild(downloadInput); + + form.submit(); + }; + + /******************************************************************************* + ** Open a server-side generated file from a url in a new window. + *******************************************************************************/ + static openInNewWindow = (url: string, filename: string) => + { + const openInWindow = window.open("", "_blank"); + openInWindow.document.write(` + + + ${filename} + + + + Opening ${filename}... +
+ +
+ + `); + }; + + } \ No newline at end of file diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index d8c1332..ea489e2 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -28,11 +28,14 @@ import "datejs"; // https://github.com/datejs/Datejs import {Chip, ClickAwayListener, Icon} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; +import {makeStyles} from "@mui/styles"; import parse from "html-react-parser"; import React, {Fragment, useReducer, useState} from "react"; import AceEditor from "react-ace"; import {Link} from "react-router-dom"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; /******************************************************************************* @@ -192,6 +195,11 @@ class ValueUtils ); } + if (field.type == QFieldType.BLOB) + { + return (); + } + return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue)); } @@ -500,9 +508,9 @@ function CodeViewer({name, mode, code}: {name: string; mode: string; code: strin
); } -//////////////////////////////////////////////////////////////////////////////////////////////// -// little private component here, for rendering an AceEditor with some buttons/controls/state // -//////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////// +// little private component here, for rendering "secret-ish" values, that you can click to reveal or copy // +//////////////////////////////////////////////////////////////////////////////////////////////////////////// function RevealComponent({fieldName, value, usage}: {fieldName: string, value: string, usage: string;}): JSX.Element { const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map); @@ -561,7 +569,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s
):( - handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} + handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off{displayValue} ) ) } @@ -570,5 +578,52 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s } +interface BlobComponentProps +{ + url: string; + filename: string; +} + +BlobComponent.defaultProps = { + foo: null, +}; + +function BlobComponent({url, filename}: BlobComponentProps): JSX.Element +{ + const download = (event: React.MouseEvent) => + { + event.stopPropagation(); + HtmlUtils.downloadUrlViaIFrame(url); + }; + + const open = (event: React.MouseEvent) => + { + event.stopPropagation(); + HtmlUtils.openInNewWindow(url, filename); + }; + + const useBlobIconStyles = makeStyles({ + blobIcon: { + marginLeft: "0.25rem", + marginRight: "0.25rem", + cursor: "pointer" + } + }) + const classes = useBlobIconStyles(); + + return ( + + {filename} + + open(e)}>open_in_new + + + download(e)}>save_alt + + + ); +} + + export default ValueUtils; From 48ebcb63c09e1b586d2e32704474a62d1aa3b47a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 30 May 2023 10:20:01 -0500 Subject: [PATCH 06/14] Updates for supporting blobs --- package.json | 2 +- src/qqq/components/audits/AuditBody.tsx | 4 ++ src/qqq/components/forms/DynamicForm.tsx | 29 +++++++++++++- src/qqq/components/forms/EntityForm.tsx | 35 ++++++++++++----- .../records/query/GridFilterOperators.tsx | 14 +++++++ src/qqq/styles/qqq-override-styles.css | 9 ++++- src/qqq/utils/DataGridUtils.tsx | 10 ++--- src/qqq/utils/qqq/ValueUtils.tsx | 39 +++++++++++-------- 8 files changed, 108 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index a3935cc..df1703b 100644 --- a/package.json +++ b/package.json @@ -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.67", + "@kingsrook/qqq-frontend-core": "1.0.68", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/audits/AuditBody.tsx b/src/qqq/components/audits/AuditBody.tsx index 072f277..f98d540 100644 --- a/src/qqq/components/audits/AuditBody.tsx +++ b/src/qqq/components/audits/AuditBody.tsx @@ -108,6 +108,10 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element { return (<>{fieldLabel}: Removed value {(oldValue)}); } + else if(message) + { + return (<>{message}); + } /* const fieldLabel = {tableMetaData?.fields?.get(fieldName)?.label ?? fieldName}; diff --git a/src/qqq/components/forms/DynamicForm.tsx b/src/qqq/components/forms/DynamicForm.tsx index 13eafb3..d98ecd2 100644 --- a/src/qqq/components/forms/DynamicForm.tsx +++ b/src/qqq/components/forms/DynamicForm.tsx @@ -19,15 +19,20 @@ * along with this program. If not, see . */ -import {colors} from "@mui/material"; +import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; +import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {colors, Icon, InputLabel} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; +import Tooltip from "@mui/material/Tooltip"; import {useFormikContext} from "formik"; import React, {useState} from "react"; import QDynamicFormField from "qqq/components/forms/DynamicFormField"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; import MDTypography from "qqq/components/legacy/MDTypography"; +import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props { @@ -35,6 +40,7 @@ interface Props formData: any; bulkEditMode?: boolean; bulkEditSwitchChangeHandler?: any; + record?: QRecord; } function QDynamicForm(props: Props): JSX.Element @@ -60,6 +66,14 @@ function QDynamicForm(props: Props): JSX.Element formikProps.setFieldValue(field.name, event.currentTarget.files[0]); }; + const removeFile = (fieldName: string) => + { + setFileName(null); + formikProps.setFieldValue(fieldName, null); + props.record?.values.delete(fieldName) + props.record?.displayValues.delete(fieldName) + }; + const bulkEditSwitchChanged = (name: string, value: boolean) => { bulkEditSwitchChangeHandler(name, value); @@ -94,10 +108,23 @@ function QDynamicForm(props: Props): JSX.Element if (field.type === "file") { + const pseudoField = new QFieldMetaData({name: fieldName, type: QFieldType.BLOB}); return ( + {field.label} + { + props.record && props.record.values.get(fieldName) && + Current File: + + {ValueUtils.getDisplayValue(pseudoField, props.record, "view")} + + removeFile(fieldName)}>delete + + + + }