add audit fields

This commit is contained in:
Sidney Alcantara
2021-10-15 17:38:47 +11:00
parent 15d0fd804d
commit 0fd3d884e3
28 changed files with 591 additions and 178 deletions

View File

@@ -13,7 +13,7 @@
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.1",
"@mdi/js": "^5.9.55",
"@mdi/js": "^6.2.95",
"@monaco-editor/react": "^4.1.0",
"@mui/icons-material": "^5.0.0",
"@mui/lab": "^5.0.0-alpha.50",

View File

@@ -0,0 +1,10 @@
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { mdiClockPlusOutline } from "@mdi/js";
export default function CreatedAt(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d={mdiClockPlusOutline} />
</SvgIcon>
);
}

View File

@@ -0,0 +1,9 @@
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
export default function UpdatedAt(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="m19.06 14.88 2.05 2-6 6.07H13v-2.01l6.06-6.06ZM12 2a10 10 0 0 1 9.98 9.373 2.561 2.561 0 0 0-2.001.047A8 8 0 0 0 4 12a8.001 8.001 0 0 0 7 7.938v2.013C5.941 21.447 2 17.164 2 12 2 6.477 6.477 2 12 2Zm9.42 11.35 1.28 1.28c.21.21.21.56 0 .77l-1 .95-2.05-2 1-1a.55.55 0 0 1 .77 0ZM12.5 7v5.25l4.018 2.384-1.051 1.045L11 13V7h1.5Z" />
</SvgIcon>
);
}

View File

@@ -2,13 +2,15 @@ import { useState, useEffect } from "react";
import _camel from "lodash/camelCase";
import { IMenuModalProps } from ".";
import { TextField } from "@mui/material";
import { TextField, Typography, Button } from "@mui/material";
import Modal from "components/Modal";
import { FieldType } from "constants/fields";
import FieldsDropdown from "./FieldsDropdown";
import { FieldType } from "constants/fields";
import { getFieldProp } from "components/fields";
import { analytics } from "analytics";
import { useProjectContext } from "contexts/ProjectContext";
export interface INewColumnProps extends IMenuModalProps {
data: Record<string, any>;
@@ -22,21 +24,52 @@ export default function NewColumn({
handleClose,
handleSave,
}: INewColumnProps) {
const { table, settingsActions } = useProjectContext();
const [columnLabel, setColumnLabel] = useState("");
const [fieldKey, setFieldKey] = useState("");
const [type, setType] = useState(FieldType.shortText);
const requireConfiguration = getFieldProp("requireConfiguration", type);
useEffect(() => {
if (type !== FieldType.id) setFieldKey(_camel(columnLabel));
}, [columnLabel]);
const isAuditField =
type === FieldType.createdBy ||
type === FieldType.createdAt ||
type === FieldType.updatedBy ||
type === FieldType.updatedAt;
useEffect(() => {
if (type === FieldType.id) {
setColumnLabel("ID");
setFieldKey("id");
if (type !== FieldType.id && !isAuditField)
setFieldKey(_camel(columnLabel));
}, [columnLabel, type, isAuditField]);
useEffect(() => {
switch (type) {
case FieldType.id:
setColumnLabel("ID");
setFieldKey("id");
break;
case FieldType.createdBy:
setColumnLabel("Created By");
setFieldKey(table?.auditFieldCreatedBy || "_createdBy");
break;
case FieldType.updatedBy:
setColumnLabel("Updated By");
setFieldKey(table?.auditFieldUpdatedBy || "_updatedBy");
break;
case FieldType.createdAt:
setColumnLabel("Created At");
setFieldKey(
(table?.auditFieldCreatedBy || "_createdBy") + ".timestamp"
);
break;
case FieldType.updatedAt:
setColumnLabel("Updated At");
setFieldKey(
(table?.auditFieldUpdatedBy || "_updatedBy") + ".timestamp"
);
break;
}
}, [type]);
}, [type, table?.auditFieldCreatedBy, table?.auditFieldUpdatedBy]);
if (!open) return null;
@@ -71,14 +104,35 @@ export default function NewColumn({
type="text"
fullWidth
onChange={(e) => setFieldKey(e.target.value)}
disabled={type === FieldType.id && fieldKey === "id"}
disabled={
(type === FieldType.id && fieldKey === "id") || isAuditField
}
helperText="Set the Firestore field key to link to this column. It will display any existing data for this field key."
sx={{ "& .MuiInputBase-input": { fontFamily: "mono" } }}
/>
</section>
<section>
<FieldsDropdown value={type} onChange={setType} />
</section>
{isAuditField && table?.audit === false && (
<section>
<Typography gutterBottom>
This field requires auditing to be enabled on this table.
</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
settingsActions?.updateTable({ id: table.id, audit: true });
}}
>
Enable auditing on this table
</Button>
</section>
)}
</>
}
actions={{

View File

@@ -255,6 +255,39 @@ export const tableSettings = (
),
},
{
type: FieldType.contentHeader,
name: "_contentHeader_audit",
label: "Auditing",
},
{
type: FieldType.checkbox,
name: "audit",
label: "Enable auditing for this table",
defaultValue: true,
assistiveText: "Track when users create or update rows",
},
{
type: FieldType.shortText,
name: "auditFieldCreatedBy",
label: "Created By field key (optional)",
defaultValue: "_createdBy",
displayCondition: "return values.audit",
assistiveText: "Optionally change the field key",
gridCols: { xs: 12, sm: 6 },
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
},
{
type: FieldType.shortText,
name: "auditFieldUpdatedBy",
label: "Updated By field key (optional)",
defaultValue: "_updatedBy",
displayCondition: "return values.audit",
assistiveText: "Optionally change the field key",
gridCols: { xs: 12, sm: 6 },
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
},
mode === TableSettingsDialogModes.create
? {
type: FieldType.contentHeader,
@@ -289,20 +322,46 @@ export const tableSettings = (
type: FieldType.contentSubHeader,
name: "_contentSubHeader_initialColumns",
label: "Initial columns",
}
: null,
mode === TableSettingsDialogModes.create
? {
type: FieldType.checkbox,
name: `_initialColumns.${TableFieldType.updatedBy}`,
label: "Updated By: Automatically log who updates a row",
sx: { "&&": { mb: 1 }, typography: "button", ml: 2 / 8 },
}
: null,
mode === TableSettingsDialogModes.create
? {
type: FieldType.checkbox,
name: `_initialColumns.${TableFieldType.createdBy}`,
label: "Created By: Automatically log who creates a row",
label: "Created By",
displayCondition: "return values.audit",
gridCols: 6,
disablePaddingTop: true,
}
: null,
mode === TableSettingsDialogModes.create
? {
type: FieldType.checkbox,
name: `_initialColumns.${TableFieldType.updatedBy}`,
label: "Updated By",
displayCondition: "return values.audit",
gridCols: 6,
disablePaddingTop: true,
}
: null,
mode === TableSettingsDialogModes.create
? {
type: FieldType.checkbox,
name: `_initialColumns.${TableFieldType.createdAt}`,
label: "Created At",
displayCondition: "return values.audit",
gridCols: 6,
disablePaddingTop: true,
}
: null,
mode === TableSettingsDialogModes.create
? {
type: FieldType.checkbox,
name: `_initialColumns.${TableFieldType.updatedAt}`,
label: "Updated At",
displayCondition: "return values.audit",
gridCols: 6,
disablePaddingTop: true,
}
: null,

View File

@@ -0,0 +1,34 @@
import { useWatch } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { useFieldStyles } from "components/SideDrawer/Form/utils";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function CreatedAt({ control, column }: ISideDrawerFieldProps) {
const fieldClasses = useFieldStyles();
const { table } = useProjectContext();
const value = useWatch({
control,
name: table?.auditFieldCreatedBy || "_createdBy",
});
if (!value || !value.timestamp) return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<div
className={fieldClasses.root}
style={{ fontVariantNumeric: "tabular-nums" }}
>
{dateLabel}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { IHeavyCellProps } from "../types";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function CreatedAt({ row, column }: IHeavyCellProps) {
const { table } = useProjectContext();
const value = row[table?.auditFieldCreatedBy || "_createdBy"];
if (!value || !value.timestamp) return null;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<span style={{ fontVariantNumeric: "tabular-nums" }}>{dateLabel}</span>
);
}

View File

@@ -0,0 +1,36 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "components/fields/types";
import withHeavyCell from "../_withTableCell/withHeavyCell";
import CreatedAtIcon from "assets/icons/CreatedAt";
import BasicCell from "../_BasicCell/BasicCellNull";
import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-CreatedAt" */)
);
const SideDrawerField = lazy(
() =>
import(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-CreatedAt" */
)
);
const Settings = lazy(
() =>
import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */)
);
export const config: IFieldConfig = {
type: FieldType.createdAt,
name: "Created At",
group: "Auditing",
dataType: "firebase.firestore.Timestamp",
initialValue: null,
icon: <CreatedAtIcon />,
description: "Displays the timestamp of when the row was created. Read-only.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -0,0 +1,46 @@
import { ISettingsProps } from "../types";
import { Typography, Link } from "@mui/material";
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
import MultiSelect from "@rowy/multiselect";
import { DATE_TIME_FORMAT } from "constants/dates";
import { EXTERNAL_LINKS } from "constants/externalLinks";
export default function Settings({ handleChange, config }: ISettingsProps) {
return (
<>
<MultiSelect
options={[
DATE_TIME_FORMAT,
"yyyy/MM/dd HH:mm",
"dd/MM/yyyy HH:mm",
"dd/MM/yyyy hh:mm aa",
"MM/dd/yyyy hh:mm aa",
]}
itemRenderer={(option) => (
<Typography sx={{ fontFamily: "mono" }}>{option.label}</Typography>
)}
label="Display format"
multiple={false}
freeText
clearable={false}
searchable={false}
value={config.format ?? DATE_TIME_FORMAT}
onChange={handleChange("format")}
TextFieldProps={{
helperText: (
<Link
href={EXTERNAL_LINKS.dateFormat}
target="_blank"
rel="noopener noreferrer"
>
Date format reference
<InlineOpenInNewIcon />
</Link>
),
}}
/>
</>
);
}

View File

@@ -1,4 +1,4 @@
import { Controller } from "react-hook-form";
import { useWatch } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { Stack, Typography, Avatar } from "@mui/material";
@@ -6,50 +6,47 @@ import { useFieldStyles } from "components/SideDrawer/Form/utils";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function User({ control, column }: ISideDrawerFieldProps) {
export default function CreatedBy({ control, column }: ISideDrawerFieldProps) {
const fieldClasses = useFieldStyles();
return (
<Controller
control={control}
name={column.key}
render={({ field: { value } }) => {
if (!value || !value.displayName || !value.timestamp)
return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
DATE_TIME_FORMAT
);
return (
<Stack
direction="row"
className={fieldClasses.root}
style={{ alignItems: "flex-start" }}
>
<Avatar
alt="Avatar"
src={value.photoURL}
sx={{ width: 32, height: 32, ml: -0.5, mr: 1.5, my: 0.5 }}
/>
const { table } = useProjectContext();
const value = useWatch({
control,
name: table?.auditFieldCreatedBy || "_createdBy",
});
<Typography
variant="body2"
component="div"
style={{ whiteSpace: "normal" }}
>
{value.displayName} ({value.email})
<Typography
variant="caption"
color="textSecondary"
component="div"
>
Created at {dateLabel}
</Typography>
</Typography>
</Stack>
);
}}
/>
if (!value || !value.displayName || !value.timestamp)
return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<Stack
direction="row"
className={fieldClasses.root}
style={{ alignItems: "flex-start" }}
>
<Avatar
alt="Avatar"
src={value.photoURL}
sx={{ width: 32, height: 32, ml: -0.5, mr: 1.5, my: 0.5 }}
/>
<Typography
variant="body2"
component="div"
style={{ whiteSpace: "normal" }}
>
{value.displayName} ({value.email})
<Typography variant="caption" color="textSecondary" component="div">
Created at {dateLabel}
</Typography>
</Typography>
</Stack>
);
}

View File

@@ -1,25 +1,31 @@
import { IHeavyCellProps } from "../types";
import { Tooltip, Chip, Avatar } from "@mui/material";
import { Tooltip, Stack, Avatar } from "@mui/material";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function CreatedBy({ row, column }: IHeavyCellProps) {
const { table } = useProjectContext();
const value = row[table?.auditFieldCreatedBy || "_createdBy"];
export default function User({ value }: IHeavyCellProps) {
if (!value || !value.displayName || !value.timestamp) return null;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
DATE_TIME_FORMAT
column.config?.format || DATE_TIME_FORMAT
);
return (
<Tooltip title={`Created at ${dateLabel}`}>
<Chip
size="small"
avatar={<Avatar alt="Avatar" src={value.photoURL} />}
label={value.displayName}
sx={{ mx: -0.25, height: 24 }}
/>
<Stack spacing={0.75} direction="row" alignItems="center">
<Avatar
alt="Avatar"
src={value.photoURL}
style={{ width: 20, height: 20 }}
/>
<span>{value.displayName}</span>
</Stack>
</Tooltip>
);
}

View File

@@ -15,19 +15,23 @@ const SideDrawerField = lazy(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-CreatedBy" */
)
);
const Settings = lazy(
() => import("./Settings" /* webpackChunkName: "Settings-CreatedBy" */)
);
export const config: IFieldConfig = {
type: FieldType.createdBy,
name: "Created By",
group: "Metadata",
group: "Auditing",
dataType:
"{ displayName: string; email: string; emailVerified: boolean; isAnonymous: boolean; photoURL: string; uid: string; timestamp: firebase.firestore.Timestamp; }",
initialValue: null,
icon: <CreatedByIcon />,
description:
"When a user creates a row, automatically logs user information and timestamp. Read-only.",
"Displays the user that created the row and timestamp. Read-only.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -15,7 +15,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
itemRenderer={(option) => (
<Typography sx={{ fontFamily: "mono" }}>{option.label}</Typography>
)}
label="Format"
label="Display format"
multiple={false}
freeText
clearable={false}

View File

@@ -21,7 +21,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
itemRenderer={(option) => (
<Typography sx={{ fontFamily: "mono" }}>{option.label}</Typography>
)}
label="Format"
label="Display format"
multiple={false}
freeText
clearable={false}

View File

@@ -0,0 +1,34 @@
import { useWatch } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { useFieldStyles } from "components/SideDrawer/Form/utils";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function UpdatedAt({ control, column }: ISideDrawerFieldProps) {
const fieldClasses = useFieldStyles();
const { table } = useProjectContext();
const value = useWatch({
control,
name: table?.auditFieldUpdatedBy || "_updatedBy",
});
if (!value || !value.timestamp) return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<div
className={fieldClasses.root}
style={{ fontVariantNumeric: "tabular-nums" }}
>
{dateLabel}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { IHeavyCellProps } from "../types";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function UpdatedBy({ row, column }: IHeavyCellProps) {
const { table } = useProjectContext();
const value = row[table?.auditFieldUpdatedBy || "_updatedBy"];
if (!value || !value.timestamp) return null;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<span style={{ fontVariantNumeric: "tabular-nums" }}>{dateLabel}</span>
);
}

View File

@@ -0,0 +1,37 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "components/fields/types";
import withHeavyCell from "../_withTableCell/withHeavyCell";
import UpdatedAtIcon from "assets/icons/UpdatedAt";
import BasicCell from "../_BasicCell/BasicCellNull";
import withSideDrawerEditor from "components/Table/editors/withSideDrawerEditor";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-UpdatedAt" */)
);
const SideDrawerField = lazy(
() =>
import(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-UpdatedAt" */
)
);
const Settings = lazy(
() =>
import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */)
);
export const config: IFieldConfig = {
type: FieldType.updatedAt,
name: "Updated At",
group: "Auditing",
dataType: "firebase.firestore.Timestamp",
initialValue: null,
icon: <UpdatedAtIcon />,
description:
"Displays the timestamp of the last update to the row. Read-only.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -1,4 +1,4 @@
import { Controller } from "react-hook-form";
import { useWatch } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { Stack, Typography, Avatar } from "@mui/material";
@@ -6,50 +6,54 @@ import { useFieldStyles } from "components/SideDrawer/Form/utils";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function User({ control, column }: ISideDrawerFieldProps) {
export default function UpdatedBy({ control, column }: ISideDrawerFieldProps) {
const fieldClasses = useFieldStyles();
return (
<Controller
control={control}
name={column.key}
render={({ field: { value } }) => {
if (!value || !value.displayName || !value.timestamp)
return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
DATE_TIME_FORMAT
);
return (
<Stack
direction="row"
className={fieldClasses.root}
style={{ alignItems: "flex-start" }}
>
<Avatar
alt="Avatar"
src={value.photoURL}
sx={{ width: 32, height: 32, ml: -0.5, mr: 1.5, my: 0.5 }}
/>
const { table } = useProjectContext();
const value = useWatch({
control,
name: table?.auditFieldUpdatedBy || "_updatedBy",
});
<Typography
variant="body2"
component="div"
style={{ whiteSpace: "normal" }}
>
{value.displayName} ({value.email})
<Typography
variant="caption"
color="textSecondary"
component="div"
>
Updated field <code>{value.updatedField}</code> at {dateLabel}
</Typography>
</Typography>
</Stack>
);
}}
/>
if (!value || !value.displayName || !value.timestamp)
return <div className={fieldClasses.root} />;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
column.config?.format || DATE_TIME_FORMAT
);
return (
<Stack
direction="row"
className={fieldClasses.root}
style={{ alignItems: "flex-start" }}
>
<Avatar
alt="Avatar"
src={value.photoURL}
sx={{ width: 32, height: 32, ml: -0.5, mr: 1.5, my: 0.5 }}
/>
<Typography
variant="body2"
component="div"
style={{ whiteSpace: "normal" }}
>
{value.displayName} ({value.email})
<Typography variant="caption" color="textSecondary" component="div">
Updated
{value.updatedField && (
<>
{" "}
field <code>{value.updatedField}</code>
</>
)}{" "}
at {dateLabel}
</Typography>
</Typography>
</Stack>
);
}

View File

@@ -1,33 +1,45 @@
import { IHeavyCellProps } from "../types";
import { Tooltip, Chip, Avatar } from "@mui/material";
import { Tooltip, Stack, Avatar } from "@mui/material";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
import { useProjectContext } from "contexts/ProjectContext";
export default function UpdatedBy({ row, column }: IHeavyCellProps) {
const { table } = useProjectContext();
const value = row[table?.auditFieldUpdatedBy || "_updatedBy"];
export default function User({ value }: IHeavyCellProps) {
if (!value || !value.displayName || !value.timestamp) return null;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
DATE_TIME_FORMAT
column.config?.format || DATE_TIME_FORMAT
);
return (
<Tooltip
title={
<>
Updated field <code>{value.updatedField}</code>
Updated
{value.updatedField && (
<>
{" "}
field <code>{value.updatedField}</code>
</>
)}
<br />
at {dateLabel}
</>
}
>
<Chip
size="small"
avatar={<Avatar alt="Avatar" src={value.photoURL} />}
label={value.displayName}
sx={{ mx: -0.25, height: 24 }}
/>
<Stack spacing={0.75} direction="row" alignItems="center">
<Avatar
alt="Avatar"
src={value.photoURL}
style={{ width: 20, height: 20 }}
/>
<span>{value.displayName}</span>
</Stack>
</Tooltip>
);
}

View File

@@ -15,19 +15,24 @@ const SideDrawerField = lazy(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-UpdatedBy" */
)
);
const Settings = lazy(
() =>
import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */)
);
export const config: IFieldConfig = {
type: FieldType.updatedBy,
name: "Updated By",
group: "Metadata",
group: "Auditing",
dataType:
"{ displayName: string; email: string; emailVerified: boolean; isAnonymous: boolean; photoURL: string; uid: string; timestamp: firebase.firestore.Timestamp; updatedField?: string; }",
initialValue: null,
icon: <UpdatedByIcon />,
description:
"When a user updates a row, automatically logs user information, timestamp, and updated field key. Read-only.",
"Displays the user that last updated the row, timestamp, and updated field key. Read-only.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -22,7 +22,7 @@ export default function User({ control, column }: ISideDrawerFieldProps) {
value.timestamp.toDate
? value.timestamp.toDate()
: value.timestamp,
DATE_TIME_FORMAT
column.config?.format || DATE_TIME_FORMAT
)
: null;

View File

@@ -1,27 +1,29 @@
import { IHeavyCellProps } from "../types";
import { Tooltip, Chip, Avatar } from "@mui/material";
import { Tooltip, Stack, Avatar } from "@mui/material";
import { format } from "date-fns";
import { DATE_TIME_FORMAT } from "constants/dates";
export default function User({ value }: IHeavyCellProps) {
export default function User({ value, column }: IHeavyCellProps) {
if (!value || !value.displayName) return null;
const chip = (
<Chip
size="small"
avatar={<Avatar alt="Avatar" src={value.photoURL} />}
label={value.displayName}
sx={{ mx: -0.25, height: 24 }}
/>
<Stack spacing={0.75} direction="row" alignItems="center">
<Avatar
alt="Avatar"
src={value.photoURL}
style={{ width: 20, height: 20 }}
/>
<span>{value.displayName}</span>
</Stack>
);
if (!value.timestamp) return chip;
const dateLabel = format(
value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp,
DATE_TIME_FORMAT
column.config?.format || DATE_TIME_FORMAT
);
return <Tooltip title={dateLabel}>{chip}</Tooltip>;

View File

@@ -13,6 +13,10 @@ const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */)
);
const Settings = lazy(
() =>
import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */)
);
export const config: IFieldConfig = {
type: FieldType.user,
@@ -26,5 +30,6 @@ export const config: IFieldConfig = {
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: withSideDrawerEditor(TableCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -32,9 +32,11 @@ import Code from "./Code";
import Action from "./Action";
import Derivative from "./Derivative";
import Aggregate from "./Aggregate";
import User from "./User";
import UpdatedBy from "./UpdatedBy";
import CreatedBy from "./CreatedBy";
import UpdatedBy from "./UpdatedBy";
import CreatedAt from "./CreatedAt";
import UpdatedAt from "./UpdatedAt";
import User from "./User";
import Id from "./Id";
import Status from "./Status";
@@ -76,10 +78,13 @@ export const FIELDS: IFieldConfig[] = [
Derivative,
Aggregate,
Status,
// AUDITING
CreatedBy,
UpdatedBy,
CreatedAt,
UpdatedAt,
// METADATA
User,
UpdatedBy,
CreatedBy,
Id,
];

View File

@@ -36,10 +36,13 @@ export enum FieldType {
derivative = "DERIVATIVE",
aggregate = "AGGREGATE",
status = "STATUS",
// AUDIT
createdBy = "CREATED_BY",
updatedBy = "UPDATED_BY",
createdAt = "CREATED_AT",
updatedAt = "UPDATED_AT",
// METADATA
user = "USER",
updatedBy = "UPDATED_BY",
createdBy = "CREATED_BY",
id = "ID",
last = "LAST",
}

View File

@@ -28,10 +28,14 @@ export type Table = {
description: string;
section: string;
tableType: "primaryCollection" | "collectionGroup";
audit?: boolean;
auditFieldCreatedBy?: string;
auditFieldUpdatedBy?: string;
};
interface IProjectContext {
tables: Table[];
table: Table;
roles: string[];
tableState: TableState;
tableActions: TableActions;
@@ -57,11 +61,12 @@ interface IProjectContext {
}) => void;
updateTable: (data: {
id: string;
collection: string;
name: string;
description: string;
roles: string[];
section: string;
name?: string;
collection?: string;
section?: string;
description?: string;
roles?: string[];
[key: string]: any;
}) => Promise<any>;
deleteTable: (id: string) => void;
};
@@ -92,6 +97,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
const { tableState, tableActions } = useTable();
const [tables, setTables] = useState<IProjectContext["tables"]>();
const [settings, settingsActions] = useSettings();
const table = _find(tables, (table) => table.id === tableState.config.id);
useEffect(() => {
const { tables } = settings;
@@ -153,19 +159,14 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
.filter((column) => column.config.required)
.map((column) => column.key);
const createdByColumn = _find(tableState.columns, [
"type",
FieldType.createdBy,
]);
if (createdByColumn)
initialData[createdByColumn.key] = rowyUser(currentUser!);
const updatedByColumn = _find(tableState.columns, [
"type",
FieldType.updatedBy,
]);
if (updatedByColumn)
initialData[updatedByColumn.key] = rowyUser(currentUser!);
if (table?.audit !== false) {
initialData[table?.auditFieldCreatedBy || "_createdBy"] = rowyUser(
currentUser!
);
initialData[table?.auditFieldUpdatedBy || "_updatedBy"] = rowyUser(
currentUser!
);
}
tableActions.row.add(
{ ...valuesFromFilter, ...initialData, ...data },
@@ -183,14 +184,12 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
const update = { [fieldName]: value };
const updatedByColumn = _find(tableState.columns, [
"type",
FieldType.updatedBy,
]);
if (updatedByColumn)
update[updatedByColumn.key] = rowyUser(currentUser!, {
updatedField: fieldName,
});
if (table?.audit !== false) {
update[table?.auditFieldUpdatedBy || "_updatedBy"] = rowyUser(
currentUser!,
{ updatedField: fieldName }
);
}
tableActions.row.update(
ref,
@@ -254,6 +253,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
settingsActions,
roles,
tables,
table,
dataGridRef,
sideDrawerRef,
columnMenuRef,

View File

@@ -45,7 +45,7 @@ export default function useSettings() {
const tableSchemaDocRef = db.doc(tableSchemaPath);
// Get columns from schemaSource if provided
let columns: Record<string, any> = [];
let columns: Record<string, any> = {};
if (schemaSource) {
const schemaSourcePath = `${
tableSettings.tableType !== "collectionGroup"
@@ -57,14 +57,18 @@ export default function useSettings() {
}
// Add columns from `_initialColumns`
for (const [type, checked] of Object.entries(data._initialColumns)) {
if (checked && !columns.some((column) => column.type === type))
columns.push({
if (
checked &&
!Object.values(columns).some((column) => column.type === type)
)
columns["_" + _camelCase(type)] = {
type,
name: getFieldProp("name", type as FieldType),
key: "_" + _camelCase(type),
fieldName: "_" + _camelCase(type),
config: {},
});
index: Object.values(columns).length,
};
}
// Appends table to settings doc
@@ -83,10 +87,12 @@ export default function useSettings() {
const updateTable = async (data: {
id: string;
name: string;
collection: string;
description: string;
roles: string[];
name?: string;
collection?: string;
section?: string;
description?: string;
roles?: string[];
[key: string]: any;
}) => {
const { tables } = settingsState;
const newTables = Array.isArray(tables) ? [...tables] : [];

View File

@@ -2362,6 +2362,11 @@
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-5.9.55.tgz#8f5bc4d924c23f30dab20545ddc768e778bbc882"
integrity sha512-BbeHMgeK2/vjdJIRnx12wvQ6s8xAYfvMmEAVsUx9b+7GiQGQ9Za8jpwp17dMKr9CgKRvemlAM4S7S3QOtEbp4A==
"@mdi/js@^6.2.95":
version "6.2.95"
resolved "https://registry.yarnpkg.com/@mdi/js/-/js-6.2.95.tgz#decf0f86035990248f25b0a4e246a7d152211273"
integrity sha512-fbD22sEBathqVSQWcxshEtzhhRNFmMnV64z6T7DClRbQ9N5axorykt3Suv2zPzLDyiqH7UhNRu0VPvPCPDNpnQ==
"@monaco-editor/loader@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.2.0.tgz#373fad69973384624e3d9b60eefd786461a76acd"