/* * 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 . */ import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; 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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import Avatar from "@mui/material/Avatar"; import Button from "@mui/material/Button"; import Card from "@mui/material/Card"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; import Divider from "@mui/material/Divider"; import Grid from "@mui/material/Grid"; import Icon from "@mui/material/Icon"; import ListItemIcon from "@mui/material/ListItemIcon"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Modal from "@mui/material/Modal"; import React, {useContext, useEffect, useReducer, useState} from "react"; import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom"; import QContext from "QContext"; import BaseLayout from "qqq/components/BaseLayout"; import DashboardWidgets from "qqq/components/DashboardWidgets"; import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons"; import QRecordSidebar from "qqq/components/QRecordSidebar"; import colors from "qqq/components/Temporary/colors"; import MDAlert from "qqq/components/Temporary/MDAlert"; import MDBox from "qqq/components/Temporary/MDBox"; import MDTypography from "qqq/components/Temporary/MDTypography"; import ProcessRun from "qqq/pages/process-run"; import QClient from "qqq/utils/QClient"; import QProcessUtils from "qqq/utils/QProcessUtils"; import QTableUtils from "qqq/utils/QTableUtils"; import QValueUtils from "qqq/utils/QValueUtils"; const qController = QClient.getInstance(); // Declaring props types for ViewForm interface Props { table?: QTableMetaData; launchProcess?: QProcessMetaData; } EntityView.defaultProps = { table: null, launchProcess: null }; function EntityView({table, launchProcess}: Props): JSX.Element { const {id} = useParams(); const location = useLocation(); const navigate = useNavigate(); const pathParts = location.pathname.split("/"); const tableName = table.name; const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [sectionFieldElements, setSectionFieldElements] = useState(null as Map); const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false); const [tableMetaData, setTableMetaData] = useState(null); const [record, setRecord] = useState(null as QRecord); const [tableSections, setTableSections] = useState([] as QTableSection[]); const [t1SectionName, setT1SectionName] = useState(null as string); const [t1SectionElement, setT1SectionElement] = useState(null as JSX.Element); const [nonT1TableSections, setNonT1TableSections] = useState([] as QTableSection[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [actionsMenu, setActionsMenu] = useState(null); const [tableWidgets, setTableWidgets] = useState([] as QWidgetMetaData[]); const [notFoundMessage, setNotFoundMessage] = useState(null); const [searchParams] = useSearchParams(); const {setPageHeader} = useContext(QContext); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [, forceUpdate] = useReducer((x) => x + 1, 0); const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); const reload = () => { setAsyncLoadInited(false); setTableMetaData(null); setRecord(null); setT1SectionElement(null); setNonT1TableSections([]); setTableProcesses([]); setTableSections(null); setTableWidgets(null); }; //////////////////////////////////////////////////////////////////////////////////////////////////// // monitor location changes - if we've clicked a link from viewing one record to viewing another, // // we'll stay in this component, but we'll need to reload all data for the new record. // // if, however, our url looks like a process, then open that process. // //////////////////////////////////////////////////////////////////////////////////////////////////// useEffect(() => { try { ///////////////////////////////////////////////////////////////// // the path for a process looks like: .../table/id/process // // so if our tableName is in the -3 index, try to open process // ///////////////////////////////////////////////////////////////// if (pathParts[pathParts.length - 3] === tableName) { const processName = pathParts[pathParts.length - 1]; const processList = allTableProcesses.filter(p => p.name.endsWith(processName)); if (processList.length > 0) { setActiveModalProcess(processList[0]); return; } else { console.log(`Couldn't find process named ${processName}`); } } } catch (e) { console.log(e); } ///////////////////////////////////////////////////////////// // if we didn't open a process, assume we need to (re)load // ///////////////////////////////////////////////////////////// reload(); setActiveModalProcess(null); }, [location.pathname]); if (!asyncLoadInited) { setAsyncLoadInited(true); (async () => { ///////////////////////////////////////////////////////////////////// // load the full table meta-data (the one we took in is a partial) // ///////////////////////////////////////////////////////////////////// const tableMetaData = await qController.loadTableMetaData(tableName); setTableMetaData(tableMetaData); ////////////////////////////////////////////////////////////////// // load top-level meta-data (e.g., to find processes for table) // ////////////////////////////////////////////////////////////////// const metaData = await qController.loadMetaData(); QValueUtils.qInstance = metaData; const processesForTable = QProcessUtils.getProcessesForTable(metaData, tableName); setTableProcesses(processesForTable); setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks) if (launchingProcess) { setLaunchingProcess(null); setActiveModalProcess(launchingProcess); } ///////////////////// // load the record // ///////////////////// let record: QRecord; try { record = await qController.get(tableName, id); setRecord(record); } catch (e) { if (e instanceof QException) { if ((e as QException).status === "404") { setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`); return; } } } setPageHeader(record.recordLabel); /////////////////////////// // load widget meta data // /////////////////////////// const matchingWidgets: QWidgetMetaData[] = []; tableMetaData.widgets && tableMetaData.widgets.forEach((widgetName) => { const widget = metaData.widgets.get(widgetName); matchingWidgets.push(widget); }); setTableWidgets(matchingWidgets); ///////////////////////////////////////////////// // define the sections, e.g., for the left-bar // ///////////////////////////////////////////////// const tableSections = QTableUtils.getSectionsForRecordSidebar(tableMetaData); setTableSections(tableSections); //////////////////////////////////////////////////// // make elements with the values for each section // //////////////////////////////////////////////////// const sectionFieldElements = new Map(); const nonT1TableSections = []; for (let i = 0; i < tableSections.length; i++) { const section = tableSections[i]; sectionFieldElements.set( section.name, { section.fieldNames.map((fieldName: string) => ( {tableMetaData.fields.get(fieldName).label}: {QValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record)} )) } , ); if (section.tier === "T1") { setT1SectionElement(sectionFieldElements.get(section.name)); setT1SectionName(section.name); } else { nonT1TableSections.push(tableSections[i]); } } setSectionFieldElements(sectionFieldElements); setNonT1TableSections(nonT1TableSections); forceUpdate(); })(); } const handleClickDeleteButton = () => { setDeleteConfirmationOpen(true); }; const handleDeleteConfirmClose = () => { setDeleteConfirmationOpen(false); }; const handleDelete = (event: { preventDefault: () => void }) => { event.preventDefault(); (async () => { await qController.delete(tableName, id) .then(() => { const path = `${pathParts.slice(0, -1).join("/")}?deleteSuccess=true`; navigate(path); }); })(); }; function processClicked(process: QProcessMetaData) { openModalProcess(process); } const renderActionsMenu = ( navigate("edit")}> edit Edit { setActionsMenu(null); handleClickDeleteButton(); }} > delete Delete {tableProcesses.length > 0 && } {tableProcesses.map((process) => ( processClicked(process)}> {process.iconName ?? "arrow_forward"} {process.label} ))} navigate("dev")}> data_object Developer Mode ); const openModalProcess = (process: QProcessMetaData = null) => { navigate(process.name); closeActionsMenu(); }; const closeModalProcess = (event: object, reason: string) => { if (reason === "backdropClick") { return; } ////////////////////////////////////////////////////////////////////////// // when closing a modal process, navigate up to the record being viewed // ////////////////////////////////////////////////////////////////////////// const newPath = location.pathname.split("/"); newPath.pop(); navigate(newPath.join("/")); }; return ( { notFoundMessage ? {notFoundMessage} : { (searchParams.get("createSuccess") || searchParams.get("updateSuccess")) ? ( {tableMetaData?.label} {" "} successfully {" "} {searchParams.get("createSuccess") ? "created" : "updated"} ) : ("") } {tableMetaData?.iconName} {tableMetaData && record ? `Viewing ${tableMetaData?.label}: ${record?.recordLabel}` : ""} {renderActionsMenu} {t1SectionElement ? ({t1SectionElement}) : null} {tableMetaData && tableMetaData.widgets && record && ( )} {nonT1TableSections.length > 0 ? nonT1TableSections.map(({ iconName, label, name, fieldNames, tier, }: any) => ( {label} {sectionFieldElements.get(name)} )) : null} {/* Delete confirmation Dialog */} Confirm Deletion Are you sure you want to delete this record? { activeModalProcess && closeModalProcess(event, reason)}>
}
}
); } export default EntityView;