diff --git a/package.json b/package.json index d2723e7..17d9795 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.99", + "@kingsrook/qqq-frontend-core": "1.0.100", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/App.tsx b/src/App.tsx index 7d2501d..fd4c6b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,6 +49,7 @@ import EntityEdit from "qqq/pages/records/edit/RecordEdit"; import RecordQuery from "qqq/pages/records/query/RecordQuery"; import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView"; import RecordView from "qqq/pages/records/view/RecordView"; +import RecordViewByUniqueKey from "qqq/pages/records/view/RecordViewByUniqueKey"; import GoogleAnalyticsUtils, {AnalyticsModel} from "qqq/utils/GoogleAnalyticsUtils"; import Client from "qqq/utils/qqq/Client"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils"; @@ -392,6 +393,13 @@ export default function App() component: , }); + routeList.push({ + name: `${app.label} View`, + key: `${app.name}.view`, + route: `${path}/key`, + component: , + }); + routeList.push({ name: `${app.label}`, key: `${app.name}.edit`, diff --git a/src/qqq/components/misc/GotoRecordDialog.tsx b/src/qqq/components/misc/GotoRecordDialog.tsx index 9b68e05..7657e7f 100644 --- a/src/qqq/components/misc/GotoRecordDialog.tsx +++ b/src/qqq/components/misc/GotoRecordDialog.tsx @@ -35,6 +35,7 @@ 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"; @@ -71,7 +72,12 @@ function hasGotoFieldNames(tableMetaData: QTableMetaData): boolean function GotoRecordDialog(props: Props): JSX.Element { - const fields: QFieldMetaData[] = []; + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is an array of array of fields. // + // that is - each entry in the top-level array is a set of fields that can be used together to goto a record // + // such as (pkey), (ukey-field1,ukey-field2). // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const options: QFieldMetaData[][] = []; let pkey = props?.tableMetaData?.fields.get(props?.tableMetaData?.primaryKeyField); let addedPkey = false; @@ -82,31 +88,38 @@ function GotoRecordDialog(props: Props): JSX.Element { for (let i = 0; i < mdbMetaData.gotoFieldNames.length; i++) { - // todo - multi-field keys!! - let fieldName = mdbMetaData.gotoFieldNames[i][0]; - let field = props.tableMetaData.fields.get(fieldName); - if (field) + const option: QFieldMetaData[] = []; + options.push(option); + for (let j = 0; j < mdbMetaData.gotoFieldNames[i].length; j++) { - fields.push(field); - - if (field.name == pkey.name) + let fieldName = mdbMetaData.gotoFieldNames[i][j]; + let field = props.tableMetaData.fields.get(fieldName); + if (field) { - addedPkey = true; + option.push(field); + + if (pkey != null && field.name == pkey.name) + { + addedPkey = true; + } } } } } } + ////////////////////////////////////////////////////////////////////////////////////////// + // if pkey wasn't in the gotoField options meta-data, go ahead add it as an option here // + ////////////////////////////////////////////////////////////////////////////////////////// if (pkey && !addedPkey) { - fields.unshift(pkey); + options.unshift([pkey]); } const makeInitialValues = () => { const rs = {} as { [field: string]: string }; - fields.forEach((field) => rs[field.name] = ""); + options.forEach((option) => option.forEach((field) => rs[field.name] = "")); return (rs); }; @@ -141,11 +154,16 @@ function GotoRecordDialog(props: Props): JSX.Element } else if (e.key == "Enter" && targetId?.startsWith("gotoInput-")) { - const index = targetId?.replaceAll("gotoInput-", ""); + const parts = targetId?.split(/-/); + const index = parts[1]; document.getElementById("gotoButton-" + index).click(); } }; + + /*************************************************************************** + ** event handler for close button + ***************************************************************************/ const closeRequested = () => { if (props.mayClose) @@ -154,10 +172,47 @@ function GotoRecordDialog(props: Props): JSX.Element } }; - const goClicked = async (fieldName: string) => + + /******************************************************************************* + ** function to say if an option's submit button should be disabled + *******************************************************************************/ + const isOptionSubmitButtonDisabled = (optionIndex: number) => + { + let anyFieldsInThisOptionHaveAValue = false; + + options[optionIndex].forEach((field) => + { + if(values[field.name]) + { + anyFieldsInThisOptionHaveAValue = true; + } + }) + + if(!anyFieldsInThisOptionHaveAValue) + { + return (true); + } + return (false); + } + + + /*************************************************************************** + ** event handler for clicking an 'option's go/submit button + ***************************************************************************/ + const optionGoClicked = async (optionIndex: number) => { setError(""); - const filter = new QQueryFilter([new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [values[fieldName]])], null, null, "AND", null, 10); + + const criteria: QFilterCriteria[] = []; + 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])}`) + }) + + const filter = new QQueryFilter(criteria, null, null, "AND", null, 10); + try { const queryResult = await qController.query(props.tableMetaData.name, filter, null, props.tableVariant); @@ -168,12 +223,26 @@ function GotoRecordDialog(props: Props): JSX.Element } else if (queryResult.length == 1) { - navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`); + if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name) + { + ///////////////////////////////////////////////// + // navigate by pkey, if that's how we searched // + ///////////////////////////////////////////////// + navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/${encodeURIComponent(queryResult[0].values.get(props.tableMetaData.primaryKeyField))}`); + } + else + { + ///////////////////////////////// + // else navigate by unique-key // + ///////////////////////////////// + navigate(`${props.metaData.getTablePathByName(props.tableMetaData.name)}/key/?${queryStringParts.join("&")}`); + } + close(); } else { - setError("More than 1 record found..."); + setError("More than 1 record was found..."); setTimeout(() => setError(""), 3000); } } @@ -187,7 +256,7 @@ function GotoRecordDialog(props: Props): JSX.Element if (props.tableMetaData) { - if (fields.length == 0 && !error) + if (options.length == 0 && !error) { setError("This table is not configured for this feature."); } @@ -200,31 +269,38 @@ function GotoRecordDialog(props: Props): JSX.Element {props.subHeader} { - fields.map((field, index) => - ( - - - {field.label} - - - handleChange(field.name, e.target.value)} - value={values[field.name]} - sx={{width: "100%"}} - onFocus={event => event.target.select()} - /> - - - goClicked(field.name)} fullWidth startIcon={double_arrow} disabled={`${values[field.name]}`.length == 0}> - Go - - - - )) + options.map((option, optionIndex) => + + { + option.map((field, index) => + ( + + + {field.label} + + + handleChange(field.name, e.target.value)} + value={values[field.name]} + sx={{width: "100%"}} + onFocus={event => event.target.select()} + /> + + + { + (index == option.length - 1) && + optionGoClicked(optionIndex)} fullWidth startIcon={double_arrow} disabled={isOptionSubmitButtonDisabled(optionIndex)}>Go + } + + + )) + } + + ) } { error && @@ -282,7 +358,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element return ( { - props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && + props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && } diff --git a/src/qqq/components/misc/RecordSidebar.tsx b/src/qqq/components/misc/RecordSidebar.tsx index fffa5fa..ba4c25b 100644 --- a/src/qqq/components/misc/RecordSidebar.tsx +++ b/src/qqq/components/misc/RecordSidebar.tsx @@ -22,7 +22,7 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; -import Box from "@mui/material/Box"; +import {Box} from "@mui/material"; import Card from "@mui/material/Card"; import Icon from "@mui/material/Icon"; import {Theme} from "@mui/material/styles"; @@ -76,12 +76,12 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P return ( - - + + { sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => ( - + document.getElementById(entry.name).scrollIntoView()} sx={{cursor: "pointer"}}> - + )) : null } diff --git a/src/qqq/pages/processes/ProcessRun.tsx b/src/qqq/pages/processes/ProcessRun.tsx index f5d1f29..f2c33e6 100644 --- a/src/qqq/pages/processes/ProcessRun.tsx +++ b/src/qqq/pages/processes/ProcessRun.tsx @@ -35,8 +35,7 @@ import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; -import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material"; -import Box from "@mui/material/Box"; +import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material"; import Card from "@mui/material/Card"; import Grid from "@mui/material/Grid"; import Step from "@mui/material/Step"; @@ -48,12 +47,14 @@ import FormData from "form-data"; import {Form, Formik} from "formik"; import parse from "html-react-parser"; import QContext from "QContext"; +import colors from "qqq/assets/theme/base/colors"; import {QCancelButton, QSubmitButton} from "qqq/components/buttons/DefaultButtons"; import QDynamicForm from "qqq/components/forms/DynamicForm"; import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import MDButton from "qqq/components/legacy/MDButton"; import MDProgress from "qqq/components/legacy/MDProgress"; import MDTypography from "qqq/components/legacy/MDTypography"; +import HelpContent from "qqq/components/misc/HelpContent"; import QRecordSidebar from "qqq/components/misc/RecordSidebar"; import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper"; import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults"; @@ -87,6 +88,14 @@ const INITIAL_RETRY_MILLIS = 1_500; const RETRY_MAX_MILLIS = 12_000; const BACKOFF_AMOUNT = 1.5; +//////////////////////////////////////////////////////////////////////////// +// define a function that we can make referenes to, which we'll overwrite // +// with formik's setFieldValue function, once we're inside formik. // +//////////////////////////////////////////////////////////////////////////// +let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void => +{ +} + function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element { const processNameParam = useParams().processName; @@ -446,6 +455,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is }); } + // todo - not ready - need process (or screen) meta-data to have helpContents... + /* + /////////////////////////////// + // screen-level help content // + /////////////////////////////// + let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"]; + const formattedHelpContent = ; + */ + return ( <> { @@ -460,6 +478,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is } + { + /* + // todo - not ready - need process (or screen) meta-data to have helpContents... + formattedHelpContent && + + {formattedHelpContent} + + */ + } + { ////////////////////////////////////////////////// // render all of the components for this screen // @@ -472,6 +500,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is helpRoles = ["EDIT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"]; } + ////////////////////////////////////////////////////////////////////////// + // if the component specifies a sub-set of field names to include, then // + // edit the formData object to just include those. // + ////////////////////////////////////////////////////////////////////////// + let formDataToUse = formData; + if(component.values && component.values.includeFieldNames) + { + formDataToUse = Object.assign({}, formData); + + formDataToUse.formFields = {}; + for (let i = 0; i < component.values.includeFieldNames.length; i++) + { + const fieldName = component.values.includeFieldNames[i]; + formDataToUse.formFields[fieldName] = formData.formFields[fieldName]; + } + } + return (
{ @@ -567,9 +612,22 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is ) } { - component.type === QComponentType.EDIT_FORM && ( - - ) + component.type === QComponentType.EDIT_FORM && + <> + { + component.values?.sectionLabel ? + + + + {component.values?.sectionLabel} + + + + + + : + } + } { component.type === QComponentType.VIEW_FORM && step.viewFields && ( @@ -1026,6 +1084,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is setProcessValues(qJobComplete.values); setQJobRunning(null); + if(formikSetFieldValueFunction) + { + ////////////////////////////////// + // reset field values in formik // + ////////////////////////////////// + for (let key in qJobComplete.values) + { + if(Object.hasOwn(formFields, key)) + { + console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`); + formikSetFieldValueFunction(key, qJobComplete.values[key]); + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const updatedFrontendStepList = qJobComplete.updatedFrontendStepList; + if(updatedFrontendStepList) + { + setSteps(updatedFrontendStepList); + } + if (activeStep && activeStep.recordListFields) { setNeedRecords(true); @@ -1385,89 +1467,98 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is > {({ values, errors, touched, isSubmitting, setFieldValue, - }) => ( -
- - { - !isWidget && ( - - - {steps.map((step) => ( - - {step.label} - - ))} - - - ) - } + }) => + { + /////////////////////////////////////////////////////////////////// + // once we're in the formik form, use its setFieldValue function // + // over top of the default one we created globally // + /////////////////////////////////////////////////////////////////// + formikSetFieldValueFunction = setFieldValue; - - - {/*************************************************************************** + return ( + + + { + !isWidget && ( + + + {steps.map((step) => ( + + {step.label} + + ))} + + + ) + } + + + + {/*************************************************************************** ** step content - e.g., the appropriate form or other screen for the step ** ***************************************************************************/} - {getDynamicStepContent( - activeStepIndex, - activeStep, - { - values, - touched, - formFields, - errors, - }, - processError, - processValues, - recordConfig, - setFieldValue, - )} - {/******************************** + {getDynamicStepContent( + activeStepIndex, + activeStep, + { + values, + touched, + formFields, + errors, + }, + processError, + processValues, + recordConfig, + setFieldValue, + )} + {/******************************** ** back &| next/submit buttons ** ********************************/} - - {true || activeStepIndex === 0 ? ( - - ) : ( - back - )} - {processError || qJobRunning || !activeStep ? ( - - ) : ( - <> - {formError && ( - - {formError} - - )} - { - noMoreSteps && - } - { - !noMoreSteps && ( - - - { - !isWidget && ( - - ) - } - - - - ) - } - - )} + + {true || activeStepIndex === 0 ? ( + + ) : ( + back + )} + {processError || qJobRunning || !activeStep ? ( + + ) : ( + <> + {formError && ( + + {formError} + + )} + { + noMoreSteps && + } + { + !noMoreSteps && ( + + + { + !isWidget && ( + + ) + } + + + + ) + } + + )} + - - - - )} +
+ + ) + }} ); diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 31e8d63..18e3220 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -867,7 +867,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init for (let i = 0; i < queryFilter?.orderBys?.length; i++) { const fieldName = queryFilter.orderBys[i].fieldName; - if (fieldName.indexOf(".") > -1) + if (fieldName != null && fieldName.indexOf(".") > -1) { const joinTableName = fieldName.replaceAll(/\..*/g, ""); if (!vjtToUse.has(joinTableName)) diff --git a/src/qqq/pages/records/view/RecordView.tsx b/src/qqq/pages/records/view/RecordView.tsx index e34bc28..75c2d3d 100644 --- a/src/qqq/pages/records/view/RecordView.tsx +++ b/src/qqq/pages/records/view/RecordView.tsx @@ -74,12 +74,14 @@ const qController = Client.getInstance(); interface Props { table?: QTableMetaData; + record?: QRecord; launchProcess?: QProcessMetaData; } RecordView.defaultProps = { table: null, + record: null, launchProcess: null, }; @@ -127,10 +129,39 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe } +/*************************************************************************** +** +***************************************************************************/ +export function getVisibleJoinTables(tableMetaData: QTableMetaData): Set +{ + const visibleJoinTables = new Set(); + + for (let i = 0; i < tableMetaData?.sections.length; i++) + { + const section = tableMetaData?.sections[i]; + if (section.isHidden || !section.fieldNames || !section.fieldNames.length) + { + continue; + } + + section.fieldNames.forEach((fieldName) => + { + const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName); + if (tableForField && tableForField.name != tableMetaData.name) + { + visibleJoinTables.add(tableForField.name); + } + }); + } + + return (visibleJoinTables); +} + + /******************************************************************************* ** Record View Screen component. *******************************************************************************/ -function RecordView({table, launchProcess}: Props): JSX.Element +function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.Element { const {id} = useParams(); @@ -147,7 +178,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map); const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); const [metaData, setMetaData] = useState(null as QInstance); - const [record, setRecord] = useState(null as QRecord); + const [record, setRecord] = useState(overrideRecord ?? null as QRecord); const [tableSections, setTableSections] = useState([] as QTableSection[]); const [t1Section, setT1Section] = useState(null as QTableSection); const [t1SectionName, setT1SectionName] = useState(null as string); @@ -381,31 +412,6 @@ function RecordView({table, launchProcess}: Props): JSX.Element reload(); }, [location.pathname, location.hash]); - const getVisibleJoinTables = (tableMetaData: QTableMetaData): Set => - { - const visibleJoinTables = new Set(); - - for (let i = 0; i < tableMetaData?.sections.length; i++) - { - const section = tableMetaData?.sections[i]; - if (section.isHidden || !section.fieldNames || !section.fieldNames.length) - { - continue; - } - - section.fieldNames.forEach((fieldName) => - { - const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName); - if (tableForField && tableForField.name != tableMetaData.name) - { - visibleJoinTables.add(tableForField.name); - } - }); - } - - return (visibleJoinTables); - }; - /******************************************************************************* ** get an element (or empty) to use as help content for a section @@ -481,7 +487,18 @@ function RecordView({table, launchProcess}: Props): JSX.Element let record: QRecord; try { - record = await qController.get(tableName, id, tableVariant, null, queryJoins); + //////////////////////////////////////////////////////////////////////////// + // if the component took in a record object, then we don't need to GET it // + //////////////////////////////////////////////////////////////////////////// + if(overrideRecord) + { + record = overrideRecord; + } + else + { + record = await qController.get(tableName, id, tableVariant, null, queryJoins); + } + setRecord(record); recordAnalytics({category: "tableEvents", action: "view", label: tableMetaData?.label + " / " + record?.recordLabel}); } @@ -518,7 +535,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element setPageHeader(record.recordLabel); - if (!launchingProcess) + if (!launchingProcess && !activeModalProcess) { try { diff --git a/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx b/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx new file mode 100644 index 0000000..5c94d08 --- /dev/null +++ b/src/qqq/pages/records/view/RecordViewByUniqueKey.tsx @@ -0,0 +1,163 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; +import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; +import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin"; +import {Alert, Box} from "@mui/material"; +import Grid from "@mui/material/Grid"; +import BaseLayout from "qqq/layouts/BaseLayout"; +import RecordView, {getVisibleJoinTables} from "qqq/pages/records/view/RecordView"; +import Client from "qqq/utils/qqq/Client"; +import TableUtils from "qqq/utils/qqq/TableUtils"; +import React, {useEffect, useState} from "react"; +import {useSearchParams} from "react-router-dom"; + +interface RecordViewByUniqueKeyProps +{ + table: QTableMetaData; +} + +RecordViewByUniqueKey.defaultProps = {}; + +const qController = Client.getInstance(); + +/*************************************************************************** + ** Wrapper around RecordView, that reads a unique key from the query string, + ** looks for a record matching that key, and shows that record. + ***************************************************************************/ +export default function RecordViewByUniqueKey({table}: RecordViewByUniqueKeyProps): JSX.Element +{ + const tableName = table.name; + + const [asyncLoadInited, setAsyncLoadInited] = useState(false); + const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); + const [doneLoading, setDoneLoading] = useState(false); + const [record, setRecord] = useState(null as QRecord); + const [errorMessage, setErrorMessage] = useState(null as string); + + const [queryParams] = useSearchParams(); + + if (!asyncLoadInited) + { + setAsyncLoadInited(true); + + (async () => + { + const tableMetaData = await qController.loadTableMetaData(tableName); + setTableMetaData(tableMetaData); + + const criteria: QFilterCriteria[] = []; + for (let [name, value] of queryParams.entries()) + { + criteria.push(new QFilterCriteria(name, QCriteriaOperator.EQUALS, [value])); + if(!tableMetaData.fields.has(name)) + { + setErrorMessage(`Query-string parameter [${name}] is not a defined field on the ${tableMetaData.label} table.`); + setDoneLoading(true); + return; + } + } + + let queryJoins: QueryJoin[] = null; + const visibleJoinTables = getVisibleJoinTables(tableMetaData); + if (visibleJoinTables.size > 0) + { + queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables); + } + + const filter = new QQueryFilter(criteria, null, null, "AND", 0, 2); + qController.query(tableName, filter, queryJoins) + .then((queryResult) => + { + setDoneLoading(true); + if (queryResult.length == 1) + { + setRecord(queryResult[0]); + } + else if (queryResult.length == 0) + { + setErrorMessage(`No ${tableMetaData.label} record was found matching the given values.`); + } + else if (queryResult.length > 1) + { + setErrorMessage(`More than one ${tableMetaData.label} record was found matching the given values.`); + } + }) + .catch((error) => + { + setDoneLoading(true); + console.log(error); + if (error && error.message) + { + setErrorMessage(error.message); + } + else if (error && error.response && error.response.data && error.response.data.error) + { + setErrorMessage(error.response.data.error); + } + else + { + setErrorMessage("Unexpected error running query"); + } + }); + })(); + } + + useEffect(() => + { + if (asyncLoadInited) + { + setAsyncLoadInited(false); + setDoneLoading(false); + setRecord(null); + } + }, [queryParams]); + + if (!doneLoading) + { + return (
Loading...
); + } + else if (record) + { + return (); + } + else if (errorMessage) + { + return ( + + + + + { + {errorMessage} + } + + + + + ); + } +} diff --git a/src/qqq/utils/qqq/TableUtils.ts b/src/qqq/utils/qqq/TableUtils.ts index 1a218c6..31df936 100644 --- a/src/qqq/utils/qqq/TableUtils.ts +++ b/src/qqq/utils/qqq/TableUtils.ts @@ -133,6 +133,11 @@ class TableUtils *******************************************************************************/ public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData] { + if(!fieldName) + { + return [null, null]; + } + if (fieldName.indexOf(".") > -1) { const nameParts = fieldName.split(".", 2); diff --git a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java index 9671353..e052739 100755 --- a/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java +++ b/src/test/java/com/kingsrook/qqq/frontend/materialdashboard/selenium/lib/QSeleniumLib.java @@ -335,7 +335,7 @@ public class QSeleniumLib return; } - if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains))) + if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains.toLowerCase()))) { LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "] containing text [" + textContains + "]"); return; @@ -345,7 +345,7 @@ public class QSeleniumLib } while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis()); - fail("Failed for non-existence of element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds."); + fail("Failed for non-existence of element matching selector [" + cssSelector + "] containing text [" + textContains + "] after [" + WAIT_SECONDS + "] seconds."); }