mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
Initial version of record audits
This commit is contained in:
256
src/qqq/components/audits/AuditBody.tsx
Normal file
256
src/qqq/components/audits/AuditBody.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||||
|
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 {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Icon from "@mui/material/Icon/Icon";
|
||||||
|
import ToggleButton from "@mui/material/ToggleButton";
|
||||||
|
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import colors from "qqq/components/legacy/colors";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
recordId: any;
|
||||||
|
record: QRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditBody.defaultProps =
|
||||||
|
{};
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||||
|
const [audits, setAudits] = useState([] as QRecord[]);
|
||||||
|
const [total, setTotal] = useState(null as number);
|
||||||
|
const [limit, setLimit] = useState(1000);
|
||||||
|
const [statusString, setStatusString] = useState("Loading audits...");
|
||||||
|
const [auditsByDate, setAuditsByDate] = useState([] as QRecord[][]);
|
||||||
|
const [sortDirection, setSortDirection] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
/////////////////////////////////
|
||||||
|
// setup filter to load audits //
|
||||||
|
/////////////////////////////////
|
||||||
|
const filter = new QQueryFilter([
|
||||||
|
new QFilterCriteria("auditTable.name", QCriteriaOperator.EQUALS, [tableMetaData.name]),
|
||||||
|
new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, [recordId]),
|
||||||
|
], [
|
||||||
|
new QFilterOrderBy("timestamp", sortDirection),
|
||||||
|
new QFilterOrderBy("id", sortDirection)
|
||||||
|
]);
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// fetch audits in try-catch //
|
||||||
|
///////////////////////////////
|
||||||
|
let audits = [] as QRecord[]
|
||||||
|
try
|
||||||
|
{
|
||||||
|
audits = await qController.query("audit", filter, limit, 0);
|
||||||
|
setAudits(audits);
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
if (e instanceof QException)
|
||||||
|
{
|
||||||
|
if ((e as QException).status === "403")
|
||||||
|
{
|
||||||
|
setStatusString("You do not have permission to view audits");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatusString("Error loading audits");
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we fetched the limit
|
||||||
|
if (audits.length == limit)
|
||||||
|
{
|
||||||
|
const count = await qController.count("audit", filter);
|
||||||
|
setTotal(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialLoadComplete(true);
|
||||||
|
|
||||||
|
const auditsByDate = [];
|
||||||
|
let thisDatesAudits = null as QRecord[];
|
||||||
|
let lastDate = null;
|
||||||
|
for (let i = 0; i < audits.length; i++)
|
||||||
|
{
|
||||||
|
const audit = audits[i];
|
||||||
|
const date = ValueUtils.formatDateTime(audit.values.get("timestamp")).split(" ")[0];
|
||||||
|
if (date != lastDate)
|
||||||
|
{
|
||||||
|
thisDatesAudits = [];
|
||||||
|
auditsByDate.push(thisDatesAudits);
|
||||||
|
lastDate = date;
|
||||||
|
}
|
||||||
|
thisDatesAudits.push(audit);
|
||||||
|
}
|
||||||
|
setAuditsByDate(auditsByDate);
|
||||||
|
|
||||||
|
///////////////////////////
|
||||||
|
// set the status string //
|
||||||
|
///////////////////////////
|
||||||
|
if (audits.length == 0)
|
||||||
|
{
|
||||||
|
setStatusString("No audits were found for this record.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (total)
|
||||||
|
{
|
||||||
|
setStatusString(`Showing first ${limit?.toLocaleString()} of ${total?.toLocaleString()} audits for this record`);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (audits.length == 1)
|
||||||
|
{
|
||||||
|
setStatusString("Showing the only audit for this record");
|
||||||
|
}
|
||||||
|
else if (audits.length == 2)
|
||||||
|
{
|
||||||
|
setStatusString("Showing the only 2 audits for this record");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setStatusString(`Showing all ${audits.length?.toLocaleString()} audits for this record`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)();
|
||||||
|
}, [sortDirection]);
|
||||||
|
|
||||||
|
const changeSortDirection = () =>
|
||||||
|
{
|
||||||
|
setAudits([]);
|
||||||
|
setSortDirection(!sortDirection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const todayFormatted = ValueUtils.formatDateTime(new Date()).split(" ")[0];
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setTime(yesterday.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
const yesterdayFormatted = ValueUtils.formatDateTime(yesterday).split(" ")[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box p={3} display="flex" flexDirection="row" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Typography variant="h5" pb={3}>
|
||||||
|
Audit for {tableMetaData.label}: {record?.recordLabel ?? recordId}
|
||||||
|
<Typography fontSize={14}>
|
||||||
|
{statusString}
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="button" pr={1}>Sort</Typography>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={sortDirection}
|
||||||
|
exclusive
|
||||||
|
onChange={changeSortDirection}
|
||||||
|
aria-label="text alignment"
|
||||||
|
>
|
||||||
|
<ToggleButton value={true} aria-label="sort ascending">
|
||||||
|
<Tooltip title="Sort by time ascending (oldest to newest)" placement="bottom">
|
||||||
|
<Icon>arrow_upward</Icon>
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value={false} aria-label="sort descending">
|
||||||
|
<Tooltip title="Sort by time descending (newest to oldest)" placement="bottom">
|
||||||
|
<Icon>arrow_downward</Icon>
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{overflow: "auto", height: "calc( 100vh - 19rem )", position: "relative"}} px={3}>
|
||||||
|
{
|
||||||
|
auditsByDate.length ? auditsByDate.map((audits) =>
|
||||||
|
{
|
||||||
|
if (audits.length)
|
||||||
|
{
|
||||||
|
const audit0 = audits[0];
|
||||||
|
const formattedTimestamp = ValueUtils.formatDateTime(audit0.values.get("timestamp"));
|
||||||
|
const timestampParts = formattedTimestamp.split(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={audit0.values.get("id")} className="auditGroupBlock">
|
||||||
|
<Box display="flex" flexDirection="row" justifyContent="center" fontSize={14}>
|
||||||
|
<Box borderTop={1} mt={1.25} mr={1} width="100%" borderColor="#B0B0B0" />
|
||||||
|
<Box whiteSpace="nowrap">
|
||||||
|
{ValueUtils.getFullWeekday(audit0.values.get("timestamp"))} {timestampParts[0]}
|
||||||
|
{timestampParts[0] == todayFormatted ? " (Today)" : ""}
|
||||||
|
{timestampParts[0] == yesterdayFormatted ? " (Yesterday)" : ""}
|
||||||
|
</Box>
|
||||||
|
<Box borderTop={1} mt={1.25} ml={1} width="100%" borderColor="#B0B0B0" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{
|
||||||
|
audits.map((audit) =>
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Box key={audit.values.get("id")} display="flex" flexDirection="row" mb={1} className="singleAuditBlock">
|
||||||
|
<Avatar sx={{bgcolor: colors.info.main, zIndex: 2}}>
|
||||||
|
<Icon>check</Icon>
|
||||||
|
</Avatar>
|
||||||
|
<Box p={1}>
|
||||||
|
<Box fontSize="0.875rem" color="rgb(123, 128, 154)">
|
||||||
|
{timestampParts[1]} {timestampParts[2]} {timestampParts[3]} {audit.displayValues.get("auditUserId")}
|
||||||
|
</Box>
|
||||||
|
<Box fontSize="1rem">
|
||||||
|
{audit.values.get("message")}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}) : <></>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuditBody;
|
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||||
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||||
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
@ -45,7 +46,8 @@ import Modal from "@mui/material/Modal";
|
|||||||
import React, {useContext, useEffect, useReducer, useState} from "react";
|
import React, {useContext, useEffect, useReducer, useState} from "react";
|
||||||
import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
|
import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/buttons/DefaultButtons";
|
import AuditBody from "qqq/components/audits/AuditBody";
|
||||||
|
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import EntityForm from "qqq/components/forms/EntityForm";
|
import EntityForm from "qqq/components/forms/EntityForm";
|
||||||
import colors from "qqq/components/legacy/colors";
|
import colors from "qqq/components/legacy/colors";
|
||||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||||
@ -86,6 +88,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
|
const [sectionFieldElements, setSectionFieldElements] = useState(null as Map<string, JSX.Element[]>);
|
||||||
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
|
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = useState(false);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null);
|
const [tableMetaData, setTableMetaData] = useState(null);
|
||||||
|
const [metaData, setMetaData] = useState(null as QInstance);
|
||||||
const [record, setRecord] = useState(null as QRecord);
|
const [record, setRecord] = useState(null as QRecord);
|
||||||
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
const [tableSections, setTableSections] = useState([] as QTableSection[]);
|
||||||
const [t1SectionName, setT1SectionName] = useState(null as string);
|
const [t1SectionName, setT1SectionName] = useState(null as string);
|
||||||
@ -103,6 +106,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
|
||||||
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
||||||
|
const [showAudit, setShowAudit] = useState(false);
|
||||||
|
|
||||||
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
|
||||||
const closeActionsMenu = () => setActionsMenu(null);
|
const closeActionsMenu = () => setActionsMenu(null);
|
||||||
@ -174,6 +178,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
|
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
|
||||||
|
// e.g., person/42/createChild/address (to create an address under person 42) //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild")
|
if(pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild")
|
||||||
{
|
{
|
||||||
@ -186,10 +191,11 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// alternatively, look for a createChild specification in the hash //
|
// alternatively, look for a createChild specification in the hash //
|
||||||
// e.g., for non-natively rendered links to open the modal. //
|
// e.g., for non-natively rendered links to open the modal. //
|
||||||
/////////////////////////////////////////////////////////////////////
|
// e.g., person/42#createChild=address (to create an address under person 42) //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
for (let i = 0; i < hashParts.length; i++)
|
for (let i = 0; i < hashParts.length; i++)
|
||||||
{
|
{
|
||||||
const parts = hashParts[i].split("=")
|
const parts = hashParts[i].split("=")
|
||||||
@ -205,6 +211,12 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(hashParts[0] == "#audit")
|
||||||
|
{
|
||||||
|
setShowAudit(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// look for anchor links - e.g., table section names. return w/ no-op if found. //
|
// look for anchor links - e.g., table section names. return w/ no-op if found. //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -247,6 +259,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
// load top-level meta-data (e.g., to find processes for table) //
|
// load top-level meta-data (e.g., to find processes for table) //
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
const metaData = await qController.loadMetaData();
|
const metaData = await qController.loadMetaData();
|
||||||
|
setMetaData(metaData);
|
||||||
ValueUtils.qInstance = metaData;
|
ValueUtils.qInstance = metaData;
|
||||||
const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName);
|
const processesForTable = ProcessUtils.getProcessesForTable(metaData, tableName);
|
||||||
setTableProcesses(processesForTable);
|
setTableProcesses(processesForTable);
|
||||||
@ -490,6 +503,17 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
<ListItemIcon><Icon>data_object</Icon></ListItemIcon>
|
<ListItemIcon><Icon>data_object</Icon></ListItemIcon>
|
||||||
Developer Mode
|
Developer Mode
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{
|
||||||
|
metaData && metaData.tables.has("audit") &&
|
||||||
|
<MenuItem onClick={() =>
|
||||||
|
{
|
||||||
|
setActionsMenu(null);
|
||||||
|
navigate("#audit")
|
||||||
|
}}>
|
||||||
|
<ListItemIcon><Icon>checklist</Icon></ListItemIcon>
|
||||||
|
Audit
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -558,6 +582,30 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
setShowEditChildForm(null);
|
setShowEditChildForm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeAudit = (event: object, reason: string) =>
|
||||||
|
{
|
||||||
|
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowAudit(false);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// navigate back up to the record being viewed //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
if(location.hash)
|
||||||
|
{
|
||||||
|
navigate(location.pathname);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const newPath = location.pathname.split("/");
|
||||||
|
newPath.pop();
|
||||||
|
navigate(newPath.join("/"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Box>
|
<Box>
|
||||||
@ -678,6 +726,24 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showAudit && tableMetaData && record &&
|
||||||
|
<Modal open={showAudit} onClose={(event, reason) => closeAudit(event, reason)}>
|
||||||
|
<div className="audit">
|
||||||
|
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
|
||||||
|
<Card sx={{my: 5, mx: "auto", pb: 0, maxWidth: "1024px"}}>
|
||||||
|
<Box component="div">
|
||||||
|
<AuditBody recordId={id} record={record} tableMetaData={tableMetaData} />
|
||||||
|
<Box p={3} display="flex" flexDirection="row" justifyContent="flex-end">
|
||||||
|
<QCancelButton label="Close" onClickHandler={() => closeAudit(null, null)} disabled={false} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -293,3 +293,21 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
color: blue;
|
color: blue;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.auditGroupBlock
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auditGroupBlock .singleAuditBlock::before
|
||||||
|
{
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
left: 19px;
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 1;
|
||||||
|
border-right: 0.0625rem solid rgb(222, 226, 230);
|
||||||
|
}
|
@ -24,7 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
|||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import "datejs";
|
import "datejs"; // https://github.com/datejs/Datejs
|
||||||
import {Box, Chip, Icon} from "@mui/material";
|
import {Box, Chip, Icon} from "@mui/material";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {Fragment} from "react";
|
import React, {Fragment} from "react";
|
||||||
@ -253,6 +253,16 @@ class ValueUtils
|
|||||||
return (`${date.toString("yyyy-MM-dd hh:mm:ss")} ${date.getHours() < 12 ? "AM" : "PM"} ${date.getTimezone()}`);
|
return (`${date.toString("yyyy-MM-dd hh:mm:ss")} ${date.getHours() < 12 ? "AM" : "PM"} ${date.getTimezone()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getFullWeekday(date: Date)
|
||||||
|
{
|
||||||
|
if(!(date instanceof Date))
|
||||||
|
{
|
||||||
|
date = new Date(date)
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return (`${date.toString("dddd")}`);
|
||||||
|
}
|
||||||
|
|
||||||
public static formatBoolean(value: any)
|
public static formatBoolean(value: any)
|
||||||
{
|
{
|
||||||
if(value === true)
|
if(value === true)
|
||||||
|
Reference in New Issue
Block a user