/* * 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Icon from "@mui/material/Icon"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip/Tooltip"; import Typography from "@mui/material/Typography"; import {DataGridPro, GridCallbackDetails, GridEventListener, GridRenderCellParams, GridRowParams, GridToolbarContainer, MuiEvent, useGridApiContext, useGridApiEventHandler} from "@mui/x-data-grid-pro"; import Widget, {AddNewRecordButton, LabelComponent, WidgetData} from "qqq/components/widgets/Widget"; import DataGridUtils from "qqq/utils/DataGridUtils"; import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; import React, {useEffect, useRef, useState} from "react"; import {Link, useNavigate} from "react-router-dom"; export interface ChildRecordListData extends WidgetData { title?: string; queryOutput?: { records: { values: any }[] }; childTableMetaData?: QTableMetaData; tablePath?: string; viewAllLink?: string; totalRows?: number; canAddChildRecord?: boolean; defaultValuesForNewChildRecords?: { [fieldName: string]: any }; disabledFieldsForNewChildRecords?: { [fieldName: string]: any }; } interface Props { widgetMetaData: QWidgetMetaData; data: ChildRecordListData; addNewRecordCallback?: () => void; disableRowClick: boolean; allowRecordEdit: boolean; editRecordCallback?: (rowIndex: number) => void; allowRecordDelete: boolean; deleteRecordCallback?: (rowIndex: number) => void; } RecordGridWidget.defaultProps = { disableRowClick: false, allowRecordEdit: false, allowRecordDelete: false }; const qController = Client.getInstance(); function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback}: Props): JSX.Element { const instance = useRef({timer: null}); const [rows, setRows] = useState([]); const [records, setRecords] = useState([] as QRecord[]); const [columns, setColumns] = useState([]); const [allColumns, setAllColumns] = useState([]); const [csv, setCsv] = useState(null as string); const [fileName, setFileName] = useState(null as string); const [gridMouseDownX, setGridMouseDownX] = useState(0); const [gridMouseDownY, setGridMouseDownY] = useState(0); const navigate = useNavigate(); useEffect(() => { if (data && data.childTableMetaData && data.queryOutput) { const records: QRecord[] = []; const queryOutputRecords = data.queryOutput.records; if (queryOutputRecords) { for (let i = 0; i < queryOutputRecords.length; i++) { records.push(new QRecord(queryOutputRecords[i])); } } const tableMetaData = new QTableMetaData(data.childTableMetaData); const rows = DataGridUtils.makeRows(records, tableMetaData, true); ///////////////////////////////////////////////////////////////////////////////// // note - tablePath may be null, if the user doesn't have access to the table. // ///////////////////////////////////////////////////////////////////////////////// const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath; const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection"); ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // capture all-columns to use for the export (before we might splice some away from the on-screen display) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////// const allColumns = [...columns]; setAllColumns(JSON.parse(JSON.stringify(columns))); //////////////////////////////////////////////////////////////// // do not not show the foreign-key column of the parent table // //////////////////////////////////////////////////////////////// if (data.defaultValuesForNewChildRecords) { for (let i = 0; i < columns.length; i++) { if (data.defaultValuesForNewChildRecords[columns[i].field]) { columns.splice(i, 1); i--; } } } //////////////////////////////////// // add actions cell, if available // //////////////////////////////////// if (allowRecordEdit || allowRecordDelete) { columns.unshift({ field: "_actions", type: "string", headerName: "Actions", sortable: false, filterable: false, width: allowRecordEdit && allowRecordDelete ? 80 : 50, renderCell: ((params: GridRenderCellParams) => { return {allowRecordEdit && editRecordCallback(params.row.__rowIndex)}>edit} {allowRecordDelete && deleteRecordCallback(params.row.__rowIndex)}>delete} ; }) }); } setRows(rows); setRecords(records); setColumns(columns); let csv = ""; for (let i = 0; i < allColumns.length; i++) { csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`; } csv += "\n"; for (let i = 0; i < records.length; i++) { for (let j = 0; j < allColumns.length; j++) { const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field); csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`; } csv += "\n"; } const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; setCsv(csv); setFileName(fileName); } }, [JSON.stringify(data?.queryOutput)]); /////////////////// // view all link // /////////////////// const labelAdditionalElementsLeft: JSX.Element[] = []; if (data && data.viewAllLink) { labelAdditionalElementsLeft.push( View All ); } /////////////////// // export button // /////////////////// let isExportDisabled = true; let tooltipTitle = "Export"; if (data && data.childTableMetaData && data.queryOutput && data.queryOutput.records && data.queryOutput.records.length > 0) { isExportDisabled = false; if (data.totalRows && data.queryOutput.records.length < data.totalRows) { tooltipTitle = "Export these " + data.queryOutput.records.length + " records."; if (data.viewAllLink) { tooltipTitle += "\nClick View All to export all records."; } } } const onExportClick = () => { if (csv) { HtmlUtils.download(fileName, csv); } else { alert("There is no data available to export."); } }; if (widgetMetaData?.showExportButton) { labelAdditionalElementsLeft.push( ); } //////////////////// // add new button // //////////////////// const labelAdditionalComponentsRight: LabelComponent[] = []; if (data && data.canAddChildRecord) { let disabledFields = data.disabledFieldsForNewChildRecords; if (!disabledFields) { disabledFields = data.defaultValuesForNewChildRecords; } labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback)); } ///////////////////////////////////////////////////////////////// // if a grid preference window is open, ignore and reset timer // ///////////////////////////////////////////////////////////////// const handleRowClick = (params: GridRowParams, event: MuiEvent, details: GridCallbackDetails) => { if (disableRowClick) { return; } (async () => { const qInstance = await qController.loadMetaData(); let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name); if (tablePath) { tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`; DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance); } })(); }; /******************************************************************************* ** So that we can useGridApiContext to add event handlers for mouse down and ** row double-click (to make it so you don't accidentally click into records), ** we have to define a grid component, so even though we don't want a custom ** toolbar, that's why we have this (and why it returns empty) *******************************************************************************/ function CustomToolbar() { const handleMouseDown: GridEventListener<"cellMouseDown"> = (params, event, details) => { setGridMouseDownX(event.clientX); setGridMouseDownY(event.clientY); clearTimeout(instance.current.timer); }; const handleDoubleClick: GridEventListener<"rowDoubleClick"> = (event: any) => { clearTimeout(instance.current.timer); }; const apiRef = useGridApiContext(); useGridApiEventHandler(apiRef, "cellMouseDown", handleMouseDown); useGridApiEventHandler(apiRef, "rowDoubleClick", handleDoubleClick); return (); } let containerPadding = -3; if (data?.isInProcess) { containerPadding = 0; } return ( (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} onRowClick={handleRowClick} getRowId={(row) => row.__rowIndex} // getRowHeight={() => "auto"} // maybe nice? wraps values in cells... components={{ Toolbar: CustomToolbar }} // pinnedColumns={pinnedColumns} // onPinnedColumnsChange={handlePinnedColumnsChange} // pagination // paginationMode="server" // rowsPerPageOptions={[20]} // sortingMode="server" // filterMode="server" // page={pageNumber} // checkboxSelection rowCount={data && data.totalRows} // onPageSizeChange={handleRowsPerPageChange} // onStateChange={handleStateChange} // density={density} // loading={loading} // filterModel={filterModel} // onFilterModelChange={handleFilterChange} // columnVisibilityModel={columnVisibilityModel} // onColumnVisibilityModelChange={handleColumnVisibilityChange} // onColumnOrderChange={handleColumnOrderChange} // onSelectionModelChange={selectionChanged} // onSortModelChange={handleSortChange} // sortingOrder={[ "asc", "desc" ]} // sortModel={columnSortModel} /> ); } export default RecordGridWidget;