mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
update audit to include child details
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,6 +17,7 @@ yalc.lock
|
|||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/lib
|
/lib
|
||||||
|
/target
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.49",
|
"@kingsrook/qqq-frontend-core": "1.0.50",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
|
@ -26,6 +26,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
|||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Icon from "@mui/material/Icon/Icon";
|
import Icon from "@mui/material/Icon/Icon";
|
||||||
@ -52,12 +53,12 @@ const qController = Client.getInstance();
|
|||||||
|
|
||||||
function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
|
||||||
const [audits, setAudits] = useState([] as QRecord[]);
|
const [audits, setAudits] = useState([] as QRecord[]);
|
||||||
const [total, setTotal] = useState(null as number);
|
const [total, setTotal] = useState(null as number);
|
||||||
const [limit, setLimit] = useState(1000);
|
const [limit, setLimit] = useState(1000);
|
||||||
const [statusString, setStatusString] = useState("Loading audits...");
|
const [statusString, setStatusString] = useState("Loading audits...");
|
||||||
const [auditsByDate, setAuditsByDate] = useState([] as QRecord[][]);
|
const [auditsByDate, setAuditsByDate] = useState([] as QRecord[][]);
|
||||||
|
const [auditDetailMap, setAuditDetailMap] = useState(null as Map<number, string[]>)
|
||||||
const [sortDirection, setSortDirection] = useState(localStorage.getItem("audit.sortDirection") === "true");
|
const [sortDirection, setSortDirection] = useState(localStorage.getItem("audit.sortDirection") === "true");
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
@ -72,7 +73,8 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, [recordId]),
|
new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, [recordId]),
|
||||||
], [
|
], [
|
||||||
new QFilterOrderBy("timestamp", sortDirection),
|
new QFilterOrderBy("timestamp", sortDirection),
|
||||||
new QFilterOrderBy("id", sortDirection)
|
new QFilterOrderBy("id", sortDirection),
|
||||||
|
new QFilterOrderBy("auditDetail.id", true)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
///////////////////////////////
|
///////////////////////////////
|
||||||
@ -81,7 +83,7 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
let audits = [] as QRecord[]
|
let audits = [] as QRecord[]
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
audits = await qController.query("audit", filter, limit, 0);
|
audits = await qController.query("audit", filter, limit, 0, [new QueryJoin("auditDetail", true, "LEFT")]);
|
||||||
setAudits(audits);
|
setAudits(audits);
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
@ -105,8 +107,37 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
setTotal(count);
|
setTotal(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInitialLoadComplete(true);
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// group the audits by auditId (e.g., this is a list that joined audit & auditDetail, so un-flatten it) //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const unflattenedAudits: QRecord[] = []
|
||||||
|
const detailMap: Map<number, string[]> = new Map();
|
||||||
|
for (let i = 0; i < audits.length; i++)
|
||||||
|
{
|
||||||
|
let id = audits[i].values.get("id");
|
||||||
|
if(i == 0 || unflattenedAudits[unflattenedAudits.length-1].values.get("id") != id)
|
||||||
|
{
|
||||||
|
unflattenedAudits.push(audits[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let auditDetail = audits[i].values.get("auditDetail.message");
|
||||||
|
if(auditDetail)
|
||||||
|
{
|
||||||
|
if(!detailMap.has(id))
|
||||||
|
{
|
||||||
|
detailMap.set(id, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
detailMap.get(id).push(auditDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audits = unflattenedAudits;
|
||||||
|
setAuditDetailMap(detailMap);
|
||||||
|
console.log(detailMap);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// group the audits by date //
|
||||||
|
//////////////////////////////
|
||||||
const auditsByDate = [];
|
const auditsByDate = [];
|
||||||
let thisDatesAudits = null as QRecord[];
|
let thisDatesAudits = null as QRecord[];
|
||||||
let lastDate = null;
|
let lastDate = null;
|
||||||
@ -240,6 +271,16 @@ function AuditBody({tableMetaData, recordId, record}: Props): JSX.Element
|
|||||||
<Box fontSize="1rem">
|
<Box fontSize="1rem">
|
||||||
{audit.values.get("message")}
|
{audit.values.get("message")}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box fontSize="0.875rem">
|
||||||
|
<ul style={{"marginLeft": "1rem"}}>
|
||||||
|
{
|
||||||
|
auditDetailMap.get(audit.values.get("id"))?.map((detail, key) =>
|
||||||
|
{
|
||||||
|
return (<li key={key}>{detail}</li>);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -84,6 +84,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [ searchParams ] = useSearchParams();
|
const [ searchParams ] = useSearchParams();
|
||||||
|
|
||||||
|
const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(searchParams.has("deleteSuccess"));
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -1154,9 +1156,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
(tableLabel && searchParams.get("deleteSuccess")) ? (
|
(tableLabel && showSuccessfullyDeletedAlert) ? (
|
||||||
<Alert color="success" onClose={() =>
|
<Alert color="success" sx={{mb: 3}} onClose={() =>
|
||||||
{}}>
|
{
|
||||||
|
setShowSuccessfullyDeletedAlert(false);
|
||||||
|
}}>
|
||||||
{`${tableLabel} successfully deleted`}
|
{`${tableLabel} successfully deleted`}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null
|
) : null
|
||||||
|
@ -47,7 +47,7 @@ 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 AuditBody from "qqq/components/audits/AuditBody";
|
import AuditBody from "qqq/components/audits/AuditBody";
|
||||||
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QDeleteButton, QEditButton} 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";
|
||||||
@ -164,12 +164,12 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
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("=");
|
||||||
if (parts.length > 1 && parts[0] == "launchProcess")
|
if (parts.length > 1 && parts[0] == "launchProcess")
|
||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const processMetaData = await qController.loadProcessMetaData(parts[1])
|
const processMetaData = await qController.loadProcessMetaData(parts[1]);
|
||||||
setActiveModalProcess(processMetaData);
|
setActiveModalProcess(processMetaData);
|
||||||
})();
|
})();
|
||||||
return;
|
return;
|
||||||
@ -184,7 +184,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const childTable = await qController.loadTableMetaData(pathParts[pathParts.length - 1])
|
const childTable = await qController.loadTableMetaData(pathParts[pathParts.length - 1]);
|
||||||
const childId: any = null; // todo - for editing a child, not just creating one.
|
const childId: any = null; // todo - for editing a child, not just creating one.
|
||||||
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
|
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
|
||||||
})();
|
})();
|
||||||
@ -198,12 +198,12 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
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("=");
|
||||||
if (parts.length > 1 && parts[0] == "createChild")
|
if (parts.length > 1 && parts[0] == "createChild")
|
||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const childTable = await qController.loadTableMetaData(parts[1])
|
const childTable = await qController.loadTableMetaData(parts[1]);
|
||||||
const childId: any = null; // todo - for editing a child, not just creating one.
|
const childId: any = null; // todo - for editing a child, not just creating one.
|
||||||
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
|
openEditChildForm(childTable, childId, null, null); // todo - defaults & disableds
|
||||||
})();
|
})();
|
||||||
@ -292,20 +292,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
console.error("Error pushing history: " + e);
|
console.error("Error pushing history: " + e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (e instanceof QException)
|
if (e instanceof QException)
|
||||||
{
|
{
|
||||||
if ((e as QException).status === "404")
|
if ((e as QException).status === "404")
|
||||||
{
|
{
|
||||||
setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`);
|
setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`);
|
||||||
historyPurge(location.pathname)
|
historyPurge(location.pathname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if ((e as QException).status === "403")
|
else if ((e as QException).status === "403")
|
||||||
{
|
{
|
||||||
setNotFoundMessage(`You do not have permission to view ${tableMetaData.label} records`);
|
setNotFoundMessage(`You do not have permission to view ${tableMetaData.label} records`);
|
||||||
historyPurge(location.pathname)
|
historyPurge(location.pathname);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,7 +315,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName})
|
HistoryUtils.push({label: `${tableMetaData?.label}: ${record.recordLabel}`, path: location.pathname, iconName: table.iconName});
|
||||||
}
|
}
|
||||||
catch(e)
|
catch(e)
|
||||||
{
|
{
|
||||||
@ -441,7 +441,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleDelete = (event: { preventDefault: () => void }) =>
|
const handleDelete = (event: { preventDefault: () => void }) =>
|
||||||
{
|
{
|
||||||
event.preventDefault();
|
event?.preventDefault();
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
await qController.delete(tableName, id)
|
await qController.delete(tableName, id)
|
||||||
@ -509,7 +509,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
<MenuItem onClick={() =>
|
<MenuItem onClick={() =>
|
||||||
{
|
{
|
||||||
setActionsMenu(null);
|
setActionsMenu(null);
|
||||||
navigate("#audit")
|
navigate("#audit");
|
||||||
}}>
|
}}>
|
||||||
<ListItemIcon><Icon>checklist</Icon></ListItemIcon>
|
<ListItemIcon><Icon>checklist</Icon></ListItemIcon>
|
||||||
Audit
|
Audit
|
||||||
@ -585,7 +585,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const closeAudit = (event: object, reason: string) =>
|
const closeAudit = (event: object, reason: string) =>
|
||||||
{
|
{
|
||||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
if (reason === "backdropClick") // allowing esc here, as it's a non-destructive close || reason === "escapeKeyDown")
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -623,7 +623,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
successMessage ?
|
successMessage ?
|
||||||
<Alert color="success" sx={{mb: 3}} onClose={() =>
|
<Alert color="success" sx={{mb: 3}} onClose={() =>
|
||||||
{
|
{
|
||||||
setSuccessMessage(null)
|
setSuccessMessage(null);
|
||||||
}}>
|
}}>
|
||||||
{successMessage}
|
{successMessage}
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -638,7 +638,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} mb={3}>
|
<Grid item xs={12} mb={3}>
|
||||||
<Card id={t1SectionName} sx={{scrollMarginTop: "100px"}}>
|
<Card id={t1SectionName} sx={{scrollMarginTop: "100px", minHeight: "88px"}}>
|
||||||
<Box display="flex" p={3} pb={1}>
|
<Box display="flex" p={3} pb={1}>
|
||||||
<Box mr={1.5}>
|
<Box mr={1.5}>
|
||||||
<Avatar sx={{bgcolor: colors.info.main}}>
|
<Avatar sx={{bgcolor: colors.info.main}}>
|
||||||
|
Reference in New Issue
Block a user