From 3f0316584043de8bd164729c5db88eab741dd217 Mon Sep 17 00:00:00 2001 From: Fabricio Godoi <7767910+fabricio-godoi@users.noreply.github.com> Date: Thu, 26 Aug 2021 11:55:18 -0300 Subject: [PATCH 01/64] Fix path separator to fetch data --- www/src/components/Table/TableHeader/Export/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/src/components/Table/TableHeader/Export/index.tsx b/www/src/components/Table/TableHeader/Export/index.tsx index 5aea53c71..2b90104ea 100644 --- a/www/src/components/Table/TableHeader/Export/index.tsx +++ b/www/src/components/Table/TableHeader/Export/index.tsx @@ -66,9 +66,10 @@ export default function Export() { const { tableState } = useFiretableContext(); const query: any = useMemo(() => { + const _path = tableState?.tablePath!.replaceAll("~2F", "/") ?? "" let _query = isCollectionGroup() - ? db.collectionGroup(tableState?.tablePath!) - : db.collection(tableState?.tablePath!); + ? db.collectionGroup(_path) + : db.collection(_path); // add filters tableState?.filters.forEach((filter) => { _query = _query.where( @@ -109,7 +110,7 @@ export default function Export() { <> {(tableState?.filters && tableState?.filters.length !== 0) || - (tableState?.orderBy && tableState?.orderBy.length !== 0) + (tableState?.orderBy && tableState?.orderBy.length !== 0) ? "The filters and sorting applied to the table will be used in the export" : "No filters or sorting will be applied on the exported data"} From f49a41287cc527449022148b06277ca97ef409b8 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 1 Oct 2021 11:37:48 +1000 Subject: [PATCH 02/64] DateTime cell: fix incorrect picker import --- src/components/fields/DateTime/TableCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/DateTime/TableCell.tsx b/src/components/fields/DateTime/TableCell.tsx index ac36da2d2..7bae0f458 100644 --- a/src/components/fields/DateTime/TableCell.tsx +++ b/src/components/fields/DateTime/TableCell.tsx @@ -1,7 +1,7 @@ import { useDebouncedCallback } from "use-debounce"; import { IHeavyCellProps } from "../types"; -import DateTimePicker from "@mui/lab/MobileDateTimePicker"; +import DateTimePicker from "@mui/lab/DateTimePicker"; import { TextField } from "@mui/material"; import { transformValue, sanitizeValue } from "../Date/utils"; From cf529e7263b330373057d664e891d58d05f8fa71 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 1 Oct 2021 12:01:54 +1000 Subject: [PATCH 03/64] ColumnHeader: fix tooltip position --- src/components/Table/ColumnHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Table/ColumnHeader.tsx b/src/components/Table/ColumnHeader.tsx index c5e720b12..817ab7a7f 100644 --- a/src/components/Table/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader.tsx @@ -66,7 +66,7 @@ const useStyles = makeStyles((theme) => background: theme.palette.background.default, color: theme.palette.text.primary, - margin: "-42px 0 0 !important", + margin: "-41px 0 0 !important", padding: theme.spacing(0, 1.5, 0, 0), "& *": { lineHeight: "40px" }, From 68bb3a72e3f9eb339925defd992e1073ff81a1ad Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 1 Oct 2021 12:08:58 +1000 Subject: [PATCH 04/64] add filters types to src/components/fiedls --- src/components/fields/types.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 7f944ed3c..e99de0d65 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -21,6 +21,7 @@ export interface IFieldConfig { TableEditor: React.ComponentType>; SideDrawerField: React.ComponentType; settings?: React.ComponentType; + filters?: React.ComponentType; csvExportFormatter?: (value: any, config?: any) => string; csvImportParser?: (value: string, config?: any) => any; } @@ -59,3 +60,9 @@ export interface ISettingsProps { tables: any; [key: string]: any; } + +// TODO: WRITE TYPES +export interface IFiltersProps { + handleChange: (key: string) => (value: any) => void; + [key: string]: any; +} From dac175bd148abb802c3ca56c810271a7e2ea4935 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 1 Oct 2021 15:47:26 +1000 Subject: [PATCH 05/64] /deploy: add copy explicitly stating we cannot access data --- src/pages/Deploy.tsx | 226 +++++++++++++++++++++++++------------------ 1 file changed, 131 insertions(+), 95 deletions(-) diff --git a/src/pages/Deploy.tsx b/src/pages/Deploy.tsx index d922f2e54..bd1d106f7 100644 --- a/src/pages/Deploy.tsx +++ b/src/pages/Deploy.tsx @@ -1,11 +1,15 @@ import { useState } from "react"; import { useLocation } from "react-router-dom"; import queryString from "query-string"; +import { SwitchTransition } from "react-transition-group"; import { useMediaQuery, Stack, Typography, + Grow, + Button, + Collapse, Link, FormControl, FormLabel, @@ -31,6 +35,7 @@ export default function DeployPage() { const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("md")); + const [continued, setContinued] = useState(false); const [confirmProject, setConfirmProject] = useState(false); const [confirmFirestore, setConfirmFirestore] = useState(false); const [confirmAuth, setConfirmAuth] = useState(false); @@ -47,107 +52,138 @@ export default function DeployPage() { title="Get started" description={ <> - - +
+ We have no access to your data and it always stays on your + project. + + } + > + + {!continued ? ( + + + + ) : ( + + <> + + + Make sure you have the following: + + + + setConfirmProject(e.target.checked) + } + /> + } + label="A Google Cloud or Firebase project" /> - } - label="A Google Cloud or Firebase project" - /> - setConfirmFirestore(e.target.checked)} + + setConfirmFirestore(e.target.checked) + } + /> + } + label="Firestore enabled" /> - } - label="Firestore enabled" - /> - setConfirmAuth(e.target.checked)} + setConfirmAuth(e.target.checked)} + /> + } + label="Firebase Authentication with the Google sign-in method enabled" /> - } - label="Firebase Authentication with the Google sign-in method enabled" - /> - - + +
+ + Don’t have a project? Follow our{" "} + + step-by-step guide + {" "} + to get started. + - - Don’t have a project? Follow our{" "} - - step-by-step guide - {" "} - to get started. - - - } - > - {confirmProject && confirmFirestore && confirmAuth ? ( - - Run on Google Cloud - - ) : ( - Run on Google Cloud - )} +
+ {confirmProject && confirmFirestore && confirmAuth ? ( + + Run on Google Cloud + + ) : ( + Run on Google Cloud + )} +
- - By setting up {name}, you agree to our{" "} - - Terms and Conditions - {" "} - and{" "} - - Privacy Policy - - . - + + By setting up {name}, you agree to our{" "} + + Terms and Conditions + {" "} + and{" "} + + Privacy Policy + + . + + + + )} + From aec3d45bbed95214b40d1689a94c402617cf9b09 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Fri, 1 Oct 2021 18:19:49 +1000 Subject: [PATCH 06/64] refactoring filters --- src/components/Table/Filters/index.tsx | 307 +++------------------ src/components/fields/LongText/index.tsx | 5 + src/components/fields/ShortText/Filter.tsx | 28 ++ src/components/fields/ShortText/index.tsx | 6 + src/components/fields/types.ts | 11 +- 5 files changed, 85 insertions(+), 272 deletions(-) create mode 100644 src/components/fields/ShortText/Filter.tsx diff --git a/src/components/Table/Filters/index.tsx b/src/components/Table/Filters/index.tsx index 7ce6b232b..10bdfb248 100644 --- a/src/components/Table/Filters/index.tsx +++ b/src/components/Table/Filters/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import _find from "lodash/find"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; - +import { useForm } from "react-hook-form"; import { makeStyles, createStyles } from "@mui/styles"; import { Popover, @@ -11,78 +11,21 @@ import { Grid, MenuItem, TextField, - FormControlLabel, - Switch, Chip, } from "@mui/material"; import FilterIcon from "@mui/icons-material/FilterList"; import CloseIcon from "@mui/icons-material/Close"; - -import MultiSelect from "@rowy/multiselect"; import ButtonWithStatus from "components/ButtonWithStatus"; - import { FieldType } from "constants/fields"; import { TableFilter } from "hooks/useTable"; import { useProjectContext } from "contexts/ProjectContext"; - import { useAppContext } from "contexts/AppContext"; import { DocActions } from "hooks/useDoc"; +import { getFieldProp } from "@src/components/fields"; const getType = (column) => column.type === FieldType.derivative ? column.config.renderFieldType : column.type; -const OPERATORS = [ - { - value: "==", - label: "Equals", - compatibleTypes: [ - FieldType.phone, - FieldType.color, - FieldType.date, - FieldType.dateTime, - FieldType.shortText, - FieldType.singleSelect, - FieldType.url, - FieldType.email, - FieldType.checkbox, - ], - }, - { - value: "in", - label: "matches any of", - compatibleTypes: [FieldType.singleSelect], - }, - // { - // value: "array-contains", - // label: "includes", - // compatibleTypes: [FieldType.connectTable], - // }, - // { - // value: "array-contains", - // label: "Has", - // compatibleTypes: [FieldType.multiSelect], - // }, - { - value: "array-contains-any", - label: "Has any", - compatibleTypes: [FieldType.multiSelect, FieldType.connectTable], - }, - { value: "<", label: "<", compatibleTypes: [FieldType.number] }, - { value: "<=", label: "<=", compatibleTypes: [FieldType.number] }, - { value: "==", label: "==", compatibleTypes: [FieldType.number] }, - { value: ">=", label: ">=", compatibleTypes: [FieldType.number] }, - { value: ">", label: ">", compatibleTypes: [FieldType.number] }, - { - value: "<", - label: "before", - compatibleTypes: [FieldType.date, FieldType.dateTime], - }, - { - value: ">=", - label: "after", - compatibleTypes: [FieldType.date, FieldType.dateTime], - }, -]; const useStyles = makeStyles((theme) => createStyles({ @@ -127,14 +70,6 @@ const useStyles = makeStyles((theme) => }) ); -const UNFILTERABLES = [ - FieldType.image, - FieldType.file, - FieldType.action, - FieldType.subTable, - FieldType.last, - FieldType.longText, -]; const Filters = () => { const { tableState, tableActions } = useProjectContext(); const { userDoc } = useAppContext(); @@ -150,7 +85,7 @@ const Filters = () => { } }, [userDoc.state, tableState?.tablePath]); const filterColumns = _sortBy(Object.values(tableState!.columns), "index") - .filter((c) => !UNFILTERABLES.includes(c.type)) + .filter((c) => getFieldProp("filter", c.type)) .map((c) => ({ key: c.key, label: c.name, @@ -159,7 +94,6 @@ const Filters = () => { ...c, })); const classes = useStyles(); - const filters = []; const [selectedColumn, setSelectedColumn] = useState(); @@ -169,52 +103,24 @@ const Filters = () => { value: "", }); + const [operators, setOperators] = useState([]); + const type = selectedColumn ? getType(selectedColumn) : null; + //const [filter, setFilter] = useState(); + const customFieldInput = type ? getFieldProp("SideDrawerField", type) : null; useEffect(() => { if (selectedColumn) { + const _filter = getFieldProp("filter", selectedColumn.type); + //setFilter(_filter) + setOperators(_filter?.operators ?? []); let updatedQuery: TableFilter = { key: selectedColumn.key, - operator: "", - value: "", + operator: _filter.operators[0].value, + value: _filter.defaultValue, }; - const type = getType(selectedColumn); - if ( - [ - FieldType.phone, - FieldType.shortText, - FieldType.url, - FieldType.email, - FieldType.checkbox, - ].includes(type) - ) { - updatedQuery = { ...updatedQuery, operator: "==" }; - } - if (type === FieldType.checkbox) { - updatedQuery = { ...updatedQuery, value: false }; - } - if (type === FieldType.connectTable) { - updatedQuery = { - key: `${selectedColumn.key}ID`, - operator: "array-contains-any", - value: [], - }; - } - if (type === FieldType.multiSelect) { - updatedQuery = { - ...updatedQuery, - operator: "array-contains-any", - value: [], - }; - } setQuery(updatedQuery); } }, [selectedColumn]); - const operators = selectedColumn - ? OPERATORS.filter((operator) => - operator.compatibleTypes.includes(getType(selectedColumn)) - ) - : []; - const [anchorEl, setAnchorEl] = useState(null); const handleClose = () => setAnchorEl(null); @@ -230,133 +136,6 @@ const Filters = () => { const id = open ? "simple-popper" : undefined; - const renderInputField = (selectedColumn, operator) => { - const type = getType(selectedColumn); - switch (type) { - case FieldType.checkbox: - return ( - { - setQuery((query) => ({ ...query, value: e.target.checked })); - }} - size="medium" - /> - } - label="Value" - labelPlacement="top" - componentsProps={{ - typography: { variant: "button" }, - }} - sx={{ - mr: 0, - ml: -0.5, - position: "relative", - bottom: -2, - "& .MuiFormControlLabel-label": { mt: 0, mb: -1 / 8, ml: 0.75 }, - }} - /> - ); - case FieldType.email: - case FieldType.phone: - case FieldType.shortText: - case FieldType.longText: - case FieldType.url: - return ( - { - const value = e.target.value; - if (value) setQuery((query) => ({ ...query, value: value })); - }} - placeholder="Text value" - /> - ); - case FieldType.number: - return ( - { - const value = e.target.value; - if (query.value || value) - setQuery((query) => ({ - ...query, - value: value !== "" ? parseFloat(value) : "", - })); - }} - value={typeof query.value === "number" ? query.value : ""} - type="number" - placeholder="Number value" - /> - ); - - case FieldType.singleSelect: - if (operator === "in") - return ( - setQuery((query) => ({ ...query, value }))} - options={ - selectedColumn.config.options - ? selectedColumn.config.options.sort() - : [] - } - value={Array.isArray(query?.value) ? query.value : []} - /> - ); - - return ( - { - if (value !== null) setQuery((query) => ({ ...query, value })); - }} - options={ - selectedColumn.config.options - ? selectedColumn.config.options.sort() - : [] - } - value={typeof query?.value === "string" ? query.value : null} - /> - ); - - case FieldType.multiSelect: - return ( - setQuery((query) => ({ ...query, value }))} - value={query.value as string[]} - max={10} - options={ - selectedColumn.config.options - ? selectedColumn.config.options.sort() - : [] - } - searchable={false} - freeText={true} - /> - ); - - case FieldType.date: - case FieldType.dateTime: - return <>//TODO:Date/Time picker; - default: - return <>Not available; - // return ; - break; - } - }; - const handleUpdateFilters = (filters: TableFilter[]) => { userDoc.dispatch({ action: DocActions.update, @@ -365,6 +144,14 @@ const Filters = () => { }, }); }; + + const { control } = useForm({ + mode: "onBlur", + // defaultValues: { + // [fieldName]: + // config.defaultValue?.value ?? getFieldProp("initialValue", _type), + // }, + }); return ( <> @@ -410,38 +197,7 @@ const Filters = () => { -
- {/* - - Results match - - - - - all - any - - - - - of the filter criteria. - - */} - { ))} - - {query.operator && - renderInputField(selectedColumn, query.operator)} + {/* {query.operator&&React.createElement(filter.input,{ + handleChange: (value) => setQuery((query) => ({ + ...query, + value, + })) + , + key: query.key, + },null)} */} + + {customFieldInput && + React.createElement(customFieldInput, { + column: selectedColumn, + control, + docRef: {}, + disabled: false, + })} - { value: "", }); setSelectedColumn(null); - //handleClose(); }} > Clear - + ), + } + ); } ); } }, []); - const [localValue, setLocalValue] = useState( - Array.isArray(value) ? value : [] - ); + const filters = config.filters ? config.filters.replace(/\{\{(.*?)\}\}/g, replacer(row)) : ""; - const algoliaIndex = config.index; + const algoliaIndex = config.index; const [algoliaSearchKeys, setAlgoliaSearchKeys] = useAlgoliaSearchKeys( {} ); @@ -150,54 +165,97 @@ export default function ConnectTableSelect({ value: hit.objectID, })); - // Pass a list of objectIDs to MultiSelect - const sanitisedValue = localValue.map( - (item) => item.docPath.split("/")[item.docPath.split("/").length - 1] - ); - - const handleChange = (_newValue) => { - // Ensure we return an array - const newValue = Array.isArray(_newValue) - ? _newValue - : _newValue !== null - ? [_newValue] + // Store a local copy of the value so the dropdown doesn’t automatically close + // when the user selects a new item and we allow for multiple selections + let initialLocalValue: any; + if (config.multiple !== false) { + initialLocalValue = Array.isArray(value) + ? value + : value?.docPath + ? [value] : []; + } else { + initialLocalValue = Array.isArray(value) + ? value[0] + : value?.docPath + ? value + : null; + } + const [localValue, setLocalValue] = useState(initialLocalValue); - // Calculate new value - const newLocalValue = newValue.map((objectID) => { - // If this objectID is already in the previous value, use that previous - // value’s snapshot (in case it points to an object not in the current - // Algolia query) - const existingMatch = _find(localValue, { - docPath: `${algoliaIndex}/${objectID}`, - }); - if (existingMatch) return existingMatch; - - // If this is a completely new selection, grab the snapshot from the - // current Algolia query - const match = _find(algoliaState.hits, { objectID }); - const { _highlightResult, ...snapshot } = match; - - // Use snapshotFields to limit snapshots - let partialSnapshot = snapshot; - if ( - Array.isArray(config.snapshotFields) && - config.snapshotFields.length > 0 - ) - partialSnapshot = _pick(snapshot, config.snapshotFields); - - return { - snapshot: partialSnapshot, - docPath: `${algoliaIndex}/${snapshot.objectID}`, - }; - }); + // Pass objectID[] | objectID | null to MultiSelect + const sanitisedValue = + config.multiple !== false + ? localValue.map((item) => item.docPath.split("/").pop()) + : localValue?.docPath?.split("/").pop() ?? null; + + const handleChange = (_newValue: string[] | string | null) => { + let newLocalValue: any; + if (config.multiple !== false && Array.isArray(_newValue)) { + newLocalValue = (_newValue as string[]) + .map((objectID) => { + const docPath = `${algoliaIndex}/${objectID}`; + + // Try to find the snapshot from the current Algolia query + const match = _find(algoliaState.hits, { objectID }); + + // If not found and this objectID is already in the previous value, + // use that previous value’s snapshot + // Else return null + if (!match) { + const existingMatch = _find(localValue, { docPath }); + if (existingMatch) return existingMatch; + else return null; + } + + const { _highlightResult, ...snapshot } = match; + + // Use snapshotFields to limit snapshots + let partialSnapshot = snapshot; + if ( + Array.isArray(config.snapshotFields) && + config.snapshotFields.length > 0 + ) + partialSnapshot = _pick(snapshot, config.snapshotFields); + + return { snapshot: partialSnapshot, docPath }; + }) + .filter((x) => x !== null); + } else if (config.multiple === false && typeof _newValue === "string") { + const docPath = `${algoliaIndex}/${_newValue}`; + + // Try to find the snapshot from the current Algolia query + const match = _find(algoliaState.hits, { objectID: _newValue }); + + // If not found and this objectID is the previous value, use that or null + if (!match) { + if (localValue?.docPath === docPath) newLocalValue = localValue; + else newLocalValue = null; + } else { + const { _highlightResult, ...snapshot } = match; + + // Use snapshotFields to limit snapshots + let partialSnapshot = snapshot; + if ( + Array.isArray(config.snapshotFields) && + config.snapshotFields.length > 0 + ) + partialSnapshot = _pick(snapshot, config.snapshotFields); + + newLocalValue = { snapshot: partialSnapshot, docPath }; + } + } else if (config.multiple === false && _newValue === null) { + newLocalValue = null; + } + + // Store in `localValue` until user closes dropdown and triggers `handleSave` + setLocalValue(newLocalValue); // If !multiple, we MUST change the value (bypassing localValue), // otherwise `setLocalValue` won’t be called in time for the new - // `localValue` to be read by `handleSave` + // `localValue` to be read by `handleSave` because this component is + // unmounted before `handleSave` is called if (config.multiple === false) onChange(newLocalValue); - // Otherwise, `setLocalValue` until user closes dropdown - else setLocalValue(newLocalValue); }; // Save when user closes dropdown @@ -214,12 +272,10 @@ export default function ConnectTableSelect({ return ( { - setAlgoliaConfig({ - indexName: algoliaIndex, - }); + setAlgoliaConfig({ indexName: algoliaIndex }); requestDispatch({ filters }); }} onClose={handleSave} @@ -227,11 +283,27 @@ export default function ConnectTableSelect({ TextFieldProps={{ className, hiddenLabel: true, + SelectProps: { + renderValue: () => { + if (Array.isArray(localValue)) { + if (localValue.length !== 1) + return `${localValue.length} selected`; + return config.primaryKeys + ?.map((key: string) => localValue[0]?.snapshot?.[key]) + .join(" "); + } else { + if (!localValue?.snapshot) return "0 selected"; + return config.primaryKeys + ?.map((key: string) => localValue?.snapshot?.[key]) + .join(" "); + } + }, + }, ...TextFieldProps, }} label={column?.name} labelPlural={config.searchLabel} - multiple={(config.multiple ?? true) as any} + multiple={config.multiple !== false} {...({ AutocompleteProps: { loading: algoliaState.loading, @@ -243,9 +315,11 @@ export default function ConnectTableSelect({ filterOptions: () => options, }, } as any)} - countText={`${localValue.length} of ${ - algoliaState.response?.nbHits ?? "?" - }`} + countText={ + Array.isArray(localValue) + ? `${localValue.length} of ${algoliaState.response?.nbHits ?? "?"}` + : undefined + } disabled={disabled} /> ); diff --git a/src/components/fields/ConnectTable/InlineCell.tsx b/src/components/fields/ConnectTable/InlineCell.tsx index 742f4296a..3a714e106 100644 --- a/src/components/fields/ConnectTable/InlineCell.tsx +++ b/src/components/fields/ConnectTable/InlineCell.tsx @@ -28,16 +28,25 @@ export const ConnectTable = forwardRef(function ConnectTable( }} > - {Array.isArray(value) && - value.map((doc: any) => ( - + {Array.isArray(value) ? ( + value.map((item: any) => ( + doc.snapshot[key]) + .map((key: string) => item.snapshot[key]) .join(" ")} /> - ))} + )) + ) : value ? ( + + value.snapshot[key]) + .join(" ")} + /> + + ) : null} {!disabled && ( diff --git a/src/components/fields/ConnectTable/Settings.tsx b/src/components/fields/ConnectTable/Settings.tsx index 61a272c22..38e88c6bc 100644 --- a/src/components/fields/ConnectTable/Settings.tsx +++ b/src/components/fields/ConnectTable/Settings.tsx @@ -2,22 +2,34 @@ import { useEffect, useState } from "react"; import { ISettingsProps } from "../types"; import _sortBy from "lodash/sortBy"; -import { TextField } from "@mui/material"; +import { + Typography, + Link, + TextField, + FormControlLabel, + Checkbox, + FormHelperText, +} from "@mui/material"; import MultiSelect from "@rowy/multiselect"; +import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; +import WarningIcon from "@mui/icons-material/WarningAmberOutlined"; import { FieldType } from "constants/fields"; -import { db } from "../../../firebase"; +import { db } from "@src/firebase"; import { useProjectContext } from "contexts/ProjectContext"; import { TABLE_SCHEMAS } from "config/dbPaths"; +import { WIKI_LINKS } from "constants/externalLinks"; export default function Settings({ handleChange, config }: ISettingsProps) { const { tables } = useProjectContext(); const tableOptions = _sortBy( - tables?.map((t) => ({ - label: `${t.section} – ${t.name} (${t.collection})`, - value: t.id, + tables?.map((table) => ({ + label: table.name, + value: table.id, + section: table.section, + collection: table.collection, })) ?? [], - "label" + ["section", "label"] ); const [columns, setColumns] = useState< @@ -43,6 +55,18 @@ export default function Settings({ handleChange, config }: ISettingsProps) { return ( <> + + Connect Table requires additional setup.{" "} + + Instructions + + + + ) => ( + <> + {option.section} > {option.label}{" "} + {option.collection} + + )} + TextFieldProps={{ + helperText: + "Make sure this table is being synced to an Algolia index", + }} /> + + handleChange("multiple")(e.target.checked)} + /> + } + label={ + <> + Multiple selection + + {config.multiple === false ? ( + <> + Field values will be either{" "} + + {"{ docPath: string; snapshot: Record; }"} + {" "} + or null.
+ Easier to filter. + + ) : ( + <> + Field values will be an array of{" "} + + {"{ docPath: string; snapshot: Record; }"} + {" "} + or an empty array. +
+ Harder to filter. + + )} +
+ + +   Existing values in this table will not be updated + + + } + style={{ marginLeft: -10 }} + /> + { - handleChange("filters")(e.target.value); - }} + onChange={(e) => handleChange("filters")(e.target.value)} + placeholder="attribute:value AND | OR | NOT attribute:value" + id="connectTable-filters" + helperText={ + <> + Use the Algolia syntax for filters:{" "} + + Algolia documentation + + + + } /> + + ![FieldType.subTable].includes(c.type))} onChange={handleChange("snapshotFields")} + TextFieldProps={{ helperText: "Fields stored in the snapshots" }} /> + ![FieldType.subTable].includes(c.type))} onChange={handleChange("trackedFields")} + TextFieldProps={{ + helperText: + "Fields to be tracked for changes and synced to the snapshot", + }} /> ); diff --git a/src/components/fields/ConnectTable/SideDrawerField.tsx b/src/components/fields/ConnectTable/SideDrawerField.tsx index f7fc12d3c..26f10a6f2 100644 --- a/src/components/fields/ConnectTable/SideDrawerField.tsx +++ b/src/components/fields/ConnectTable/SideDrawerField.tsx @@ -18,10 +18,9 @@ export default function ConnectTable({ control={control} name={column.key} render={({ field: { onChange, onBlur, value } }) => { - const handleDelete = (hit: any) => () => { - // if (multiple) - onChange(value.filter((v) => v.snapshot.objectID !== hit.objectID)); - // else form.setFieldValue(field.name, []); + const handleDelete = (docPath: string) => () => { + if (column.config?.multiple === false) onChange(null); + else onChange(value.filter((v) => v.docPath !== docPath)); }; return ( @@ -38,26 +37,37 @@ export default function ConnectTable({ hiddenLabel: true, fullWidth: true, onBlur, - SelectProps: { - renderValue: () => `${value?.length ?? 0} selected`, - }, }} /> )} - {Array.isArray(value) && ( + {value && ( - {value.map(({ snapshot }) => ( - + {Array.isArray(value) ? ( + value.map(({ snapshot, docPath }) => ( + + snapshot[key]) + .join(" ")} + onDelete={disabled ? undefined : handleDelete(docPath)} + /> + + )) + ) : value ? ( + snapshot[key]) + ?.map((key: string) => value.snapshot[key]) .join(" ")} - onDelete={disabled ? undefined : handleDelete(snapshot)} + onDelete={ + disabled ? undefined : handleDelete(value.docPath) + } /> - ))} + ) : null} )} diff --git a/src/components/fields/ConnectTable/index.tsx b/src/components/fields/ConnectTable/index.tsx index 99d2df14c..0f505b21e 100644 --- a/src/components/fields/ConnectTable/index.tsx +++ b/src/components/fields/ConnectTable/index.tsx @@ -25,11 +25,12 @@ export const config: IFieldConfig = { type: FieldType.connectTable, name: "Connect Table (Alpha)", group: "Connection", - dataType: "{ docPath: string; snapshot: Record; }[]", + dataType: + "{ docPath: string; snapshot: Record; }[] | { docPath: string; snapshot: Record; } | null", initialValue: [], icon: , description: - "Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia integration.", + "Connects to an existing table to fetch a snapshot of values from a row. Requires Algolia setup.", TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, { anchorOrigin: { horizontal: "left", vertical: "bottom" }, transparent: true, From 055286aab4dcbca1416d13922c0dc5fe8889b45b Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 17:43:10 +1100 Subject: [PATCH 50/64] =?UTF-8?q?fix=20Side=20Drawer=20not=20loading=20the?= =?UTF-8?q?=20correct=20row=E2=80=99s=20values=20for=20some=20fields=20aft?= =?UTF-8?q?er=20clicking=20the=20navigation=20buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SideDrawer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SideDrawer/index.tsx b/src/components/SideDrawer/index.tsx index 8c29cc789..0cd164b0f 100644 --- a/src/components/SideDrawer/index.tsx +++ b/src/components/SideDrawer/index.tsx @@ -112,7 +112,7 @@ export default function SideDrawer() { (urlDocState.doc || cell) && !_isEmpty(tableState?.columns) && (
Date: Thu, 14 Oct 2021 18:01:42 +1100 Subject: [PATCH 51/64] Color: remove unused code in side drawer field --- .../fields/Color/SideDrawerField.tsx | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/components/fields/Color/SideDrawerField.tsx b/src/components/fields/Color/SideDrawerField.tsx index 9c40bb299..0cb627f3f 100644 --- a/src/components/fields/Color/SideDrawerField.tsx +++ b/src/components/fields/Color/SideDrawerField.tsx @@ -4,45 +4,15 @@ import { ISideDrawerFieldProps } from "../types"; import { ColorPicker, toColor } from "react-color-palette"; import "react-color-palette/lib/css/styles.css"; -import { makeStyles, createStyles } from "@mui/styles"; -import { ButtonBase, Box, Typography, Collapse } from "@mui/material"; +import { ButtonBase, Box, Collapse } from "@mui/material"; import { useFieldStyles } from "components/SideDrawer/Form/utils"; -const useStyles = makeStyles((theme) => - createStyles({ - root: { - height: 56, - cursor: "pointer", - textAlign: "left", - borderRadius: theme.shape.borderRadius, - - backgroundColor: - theme.palette.mode === "light" - ? "rgba(0, 0, 0, 0.09)" - : "rgba(255, 255, 255, 0.09)", - margin: 0, - width: "100%", - padding: theme.spacing(0, 0.75), - }, - - colorIndicator: { - width: 20, - height: 20, - marginLeft: 2, - - boxShadow: `0 0 0 1px ${theme.palette.text.disabled} inset`, - borderRadius: theme.shape.borderRadius, - }, - }) -); - export default function Color({ column, control, disabled, }: ISideDrawerFieldProps) { - const classes = useStyles(); const fieldClasses = useFieldStyles(); const [showPicker, setShowPicker] = useState(false); @@ -53,7 +23,6 @@ export default function Color({ control={control} name={column.key} render={({ field: { onChange, onBlur, value } }) => { - console.log(value); return ( <> Date: Thu, 14 Oct 2021 18:16:56 +1100 Subject: [PATCH 52/64] useTableData: fix connect table becoming unclearable --- src/hooks/useTable/useTableData.tsx | 25 +++++++++++++------------ src/utils/fns.ts | 13 ------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/hooks/useTable/useTableData.tsx b/src/hooks/useTable/useTableData.tsx index d797b4012..0ea6e34c8 100644 --- a/src/hooks/useTable/useTableData.tsx +++ b/src/hooks/useTable/useTableData.tsx @@ -1,22 +1,21 @@ -import { db } from "../../firebase"; -import { useSnackbar } from "notistack"; - -import Button from "@mui/material/Button"; import { useEffect, useReducer } from "react"; +import _findIndex from "lodash/findIndex"; +import _orderBy from "lodash/orderBy"; import _isEqual from "lodash/isEqual"; -import _merge from "lodash/merge"; +import _set from "lodash/set"; import firebase from "firebase/app"; -import { TableFilter, TableOrder } from "."; +import { db } from "@src/firebase"; +import { useSnackbar } from "notistack"; + +import Button from "@mui/material/Button"; +import { useAppContext } from "contexts/AppContext"; +import { TableFilter, TableOrder } from "."; import { isCollectionGroup, generateSmallerId, missingFieldsReducer, - deepen, } from "utils/fns"; -import _findIndex from "lodash/findIndex"; -import _orderBy from "lodash/orderBy"; -import { useAppContext } from "contexts/AppContext"; // Safety parameter sets the upper limit of number of docs fetched by this hook export const CAP = 1000; @@ -53,12 +52,14 @@ const rowsReducer = (prevRows: any, update: any) => { (r: any) => r.id === update.rowRef.id ); const _newRows = [...prevRows]; - _newRows[rowIndex] = _merge(_newRows[rowIndex], deepen(update.update)); + // Must not use lodash merge here. Breaks Connect Table: cannot clear value + for (const [key, value] of Object.entries(update.update)) { + _set(_newRows[rowIndex], key, value); + } const missingRequiredFields = ( prevRows[rowIndex]._missingRequiredFields ?? [] ).reduce(missingFieldsReducer(_newRows[rowIndex]), []); - if (missingRequiredFields.length === 0) { delete _newRows[rowIndex]._missingRequiredFields; update.rowRef diff --git a/src/utils/fns.ts b/src/utils/fns.ts index bdcf1f17a..5edcf8d6f 100644 --- a/src/utils/fns.ts +++ b/src/utils/fns.ts @@ -1,5 +1,4 @@ import _get from "lodash/get"; -import _set from "lodash/set"; import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "config/dbPaths"; /** @@ -119,18 +118,6 @@ export const getCellValue = (row: Record, key: string) => { return row[key]; }; -// convert dot notation to nested object -export function deepen(obj) { - const result = {}; - - // For each object path (property key) in the object - for (const objectPath in obj) { - _set(result, objectPath, obj[objectPath]); - } - - return result; -} - export function flattenObject(ob) { var toReturn = {}; From 2be55242be676afaec874a9f9b39603ee750da2f Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Thu, 14 Oct 2021 18:55:52 +1100 Subject: [PATCH 53/64] show error when rowyrun is called without a url --- src/contexts/ProjectContext.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index d192dc30f..e795217d7 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -210,7 +210,21 @@ export const ProjectContextProvider: React.FC = ({ children }) => { // rowyRun access const _rowyRun: IProjectContext["rowyRun"] = async (args) => { const authToken = await getAuthToken(); - return rowyRun({ rowyRunUrl: settings.doc.rowyRunUrl, authToken, ...args }); + if (settings.doc.rowyRunUrl) + return rowyRun({ + rowyRunUrl: settings.doc.rowyRunUrl, + authToken, + ...args, + }); + else { + enqueueSnackbar( + `The rowyRun is not setup. checkout docs for install guide`, + { + variant: "error", + } + ); + return { success: false, error: "rowyRun is not setup" }; + } }; // A ref to the data grid. Contains data grid functions From 44b0314329128e4ec908b4b8ad09d3cec4fa0701 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Thu, 14 Oct 2021 18:57:09 +1100 Subject: [PATCH 54/64] error copy --- src/contexts/ProjectContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index e795217d7..d496219bc 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -218,7 +218,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => { }); else { enqueueSnackbar( - `The rowyRun is not setup. checkout docs for install guide`, + `RowyRun is not setup. Checkout the docs for installation guide`, { variant: "error", } From 37f1c5394ef361982037da9f60e9ec102d930d92 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 19:54:24 +1100 Subject: [PATCH 55/64] fix last frozen row not showing selection outline --- src/components/Table/styles.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/Table/styles.ts b/src/components/Table/styles.ts index 9f2c10af8..989e32cb2 100644 --- a/src/components/Table/styles.ts +++ b/src/components/Table/styles.ts @@ -90,6 +90,15 @@ export const useStyles = makeStyles((theme) => .split("),") .slice(1) .join("),"), + + "&[aria-selected=true]": { + boxShadow: + theme.shadows[2] + .replace(/, 0 (\d+px)/g, ", $1 0") + .split("),") + .slice(1) + .join("),") + ", inset 0 0 0 2px var(--selection-color)", + }, }, "& .rdg-cell-copied": { From bb65f98d236af63b1465db0f7b8de27c42e42aa5 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 19:58:15 +1100 Subject: [PATCH 56/64] update missing rowy run url error copy --- src/contexts/ProjectContext.tsx | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index d496219bc..b631462f9 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -5,7 +5,10 @@ import _sortBy from "lodash/sortBy"; import _find from "lodash/find"; import firebase from "firebase/app"; -import useTable, { TableActions, TableState } from "@src/hooks/useTable"; +import { Button } from "@mui/material"; +import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; + +import useTable, { TableActions, TableState } from "hooks/useTable"; import useSettings from "hooks/useSettings"; import { useAppContext } from "./AppContext"; import { SideDrawerRef } from "components/SideDrawer"; @@ -15,6 +18,7 @@ import { ImportWizardRef } from "components/Wizards/ImportWizard"; import { rowyRun, IRowyRunRequestProps } from "utils/rowyRun"; import { FieldType } from "constants/fields"; import { rowyUser } from "utils/fns"; +import { WIKI_LINKS } from "constants/externalLinks"; export type Table = { id: string; @@ -217,12 +221,19 @@ export const ProjectContextProvider: React.FC = ({ children }) => { ...args, }); else { - enqueueSnackbar( - `RowyRun is not setup. Checkout the docs for installation guide`, - { - variant: "error", - } - ); + enqueueSnackbar(`Rowy Run is not set up`, { + variant: "error", + action: ( + + ), + }); return { success: false, error: "rowyRun is not setup" }; } }; From 713ebe6e897fbbdae5173d5fb3e46169adc220b6 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 20:01:24 +1100 Subject: [PATCH 57/64] Side Drawer: scroll to selected cell and focus if available --- .../SideDrawer/Form/FieldWrapper.tsx | 1 + src/components/SideDrawer/Form/Label.tsx | 7 +---- src/components/SideDrawer/Form/index.tsx | 30 +++++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/SideDrawer/Form/FieldWrapper.tsx b/src/components/SideDrawer/Form/FieldWrapper.tsx index e113fb3b4..ceb93c890 100644 --- a/src/components/SideDrawer/Form/FieldWrapper.tsx +++ b/src/components/SideDrawer/Form/FieldWrapper.tsx @@ -39,6 +39,7 @@ export default function FieldWrapper({ sx={{ color: "text.primary", height: 24, + scrollMarginTop: 24, "& svg": { display: "block", color: "action.active", diff --git a/src/components/SideDrawer/Form/Label.tsx b/src/components/SideDrawer/Form/Label.tsx index b6ec77a25..f6452a75b 100644 --- a/src/components/SideDrawer/Form/Label.tsx +++ b/src/components/SideDrawer/Form/Label.tsx @@ -1,10 +1,5 @@ import { makeStyles, createStyles } from "@mui/styles"; -import { - FormLabel, - FormLabelProps, - Tooltip, - IconButton, -} from "@mui/material"; +import { FormLabel, FormLabelProps, Tooltip, IconButton } from "@mui/material"; import HelpIcon from "@mui/icons-material/HelpOutline"; const useStyles = makeStyles((theme) => diff --git a/src/components/SideDrawer/Form/index.tsx b/src/components/SideDrawer/Form/index.tsx index bb70cf78b..7852882e4 100644 --- a/src/components/SideDrawer/Form/index.tsx +++ b/src/components/SideDrawer/Form/index.tsx @@ -1,4 +1,4 @@ -import { createElement } from "react"; +import { createElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; @@ -22,7 +22,7 @@ export interface IFormProps { } export default function Form({ values }: IFormProps) { - const { tableState } = useProjectContext(); + const { tableState, sideDrawerRef } = useProjectContext(); const { userDoc } = useAppContext(); const userDocHiddenFields = userDoc.state.doc?.tables?.[`${tableState!.tablePath}`]?.hiddenFields ?? []; @@ -51,17 +51,21 @@ export default function Form({ values }: IFormProps) { const { control, reset, formState, getValues } = methods; const { dirtyFields } = formState; - // const { sideDrawerRef } = useProjectContext(); - // useEffect(() => { - // const column = sideDrawerRef?.current?.cell?.column; - // if (!column) return; - - // const elem = document.getElementById(`sidedrawer-label-${column}`) - // ?.parentNode as HTMLElement; - - // // Time out for double-clicking on cells, which can open the null editor - // setTimeout(() => elem?.scrollIntoView({ behavior: "smooth" }), 200); - // }, [sideDrawerRef?.current]); + useEffect(() => { + const column = sideDrawerRef?.current?.cell?.column; + if (!column) return; + + const labelElem = document.getElementById( + `sidedrawer-label-${column}` + )?.parentElement; + const fieldElem = document.getElementById(`sidedrawer-field-${column}`); + + // Time out for double-clicking on cells, which can open the null editor + setTimeout(() => { + if (labelElem) labelElem.scrollIntoView({ behavior: "smooth" }); + if (fieldElem) fieldElem.focus({ preventScroll: true }); + }, 200); + }, [sideDrawerRef?.current]); return ( From 0a09d6bbedddf954d8b018c70b64f7d77c5306f6 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 23:50:00 +1100 Subject: [PATCH 58/64] add new step-based extensions UI --- .../index.tsx => CodeEditorHelper.tsx} | 16 +- .../TableHeader/Extensions/ExtensionList.tsx | 45 +- .../TableHeader/Extensions/ExtensionModal.tsx | 541 +++++------------- .../TableHeader/Extensions/Step1Triggers.tsx | 56 ++ .../Extensions/Step2RequiredFields.tsx | 59 ++ .../Extensions/Step3Conditions.tsx | 71 +++ .../TableHeader/Extensions/Step4Body.tsx | 77 +++ .../Table/TableHeader/Extensions/index.tsx | 28 +- .../Table/TableHeader/Extensions/utils.ts | 81 ++- src/theme/components.tsx | 10 + 10 files changed, 495 insertions(+), 489 deletions(-) rename src/components/{CodeEditorHelper/index.tsx => CodeEditorHelper.tsx} (86%) create mode 100644 src/components/Table/TableHeader/Extensions/Step1Triggers.tsx create mode 100644 src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx create mode 100644 src/components/Table/TableHeader/Extensions/Step3Conditions.tsx create mode 100644 src/components/Table/TableHeader/Extensions/Step4Body.tsx diff --git a/src/components/CodeEditorHelper/index.tsx b/src/components/CodeEditorHelper.tsx similarity index 86% rename from src/components/CodeEditorHelper/index.tsx rename to src/components/CodeEditorHelper.tsx index ec8aff396..553f29947 100644 --- a/src/components/CodeEditorHelper/index.tsx +++ b/src/components/CodeEditorHelper.tsx @@ -1,5 +1,6 @@ -import { Stack, Typography, Grid, Tooltip, Chip, Button } from "@mui/material"; +import { Stack, Typography, Grid, Tooltip, Button } from "@mui/material"; import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; + export interface ICodeEditorHelperProps { docLink: string; additionalVariables?: { @@ -42,24 +43,19 @@ export default function CodeEditorHelper({ return ( - + You can access: - + {availableVariables.concat(additionalVariables ?? []).map((v) => ( - + {v.key} ))} diff --git a/src/components/Table/TableHeader/Extensions/ExtensionList.tsx b/src/components/Table/TableHeader/Extensions/ExtensionList.tsx index e6e5b2c8b..267211d08 100644 --- a/src/components/Table/TableHeader/Extensions/ExtensionList.tsx +++ b/src/components/Table/TableHeader/Extensions/ExtensionList.tsx @@ -18,16 +18,22 @@ import { } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import ExtensionIcon from "assets/icons/Extension"; -import DuplicateIcon from "@mui/icons-material/ContentCopy"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/DeleteForever"; +import DuplicateIcon from "assets/icons/Copy"; +import EditIcon from "@mui/icons-material/EditOutlined"; +import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EmptyState from "components/EmptyState"; -import { extensionTypes, IExtension, IExtensionType } from "./utils"; +import { + extensionTypes, + extensionNames, + IExtension, + ExtensionType, +} from "./utils"; +import { DATE_TIME_FORMAT } from "constants/dates"; export interface IExtensionListProps { extensions: IExtension[]; - handleAddExtension: (type: IExtensionType) => void; + handleAddExtension: (type: ExtensionType) => void; handleUpdateActive: (index: number, active: boolean) => void; handleDuplicate: (index: number) => void; handleEdit: (index: number) => void; @@ -53,7 +59,7 @@ export default function ExtensionList({ setAnchorEl(addButtonRef.current); }; - const handleChooseAddType = (type: IExtensionType) => { + const handleChooseAddType = (type: ExtensionType) => { handleClose(); handleAddExtension(type); }; @@ -95,12 +101,8 @@ export default function ExtensionList({ transformOrigin={{ vertical: "top", horizontal: "right" }} > {extensionTypes.map((type) => ( - { - handleChooseAddType(type); - }} - > - {type} + handleChooseAddType(type)}> + {extensionNames[type]} ))} @@ -132,7 +134,14 @@ export default function ExtensionList({ children={ } secondaryAction={ @@ -182,13 +191,15 @@ export default function ExtensionList({ - Last updated by {extensionObject.lastEditor.displayName} + Last updated
- on{" "} - {format(extensionObject.lastEditor.lastUpdate, "PPPP")} + by {extensionObject.lastEditor.displayName}
at{" "} - {format(extensionObject.lastEditor.lastUpdate, "pppp")} + {format( + extensionObject.lastEditor.lastUpdate, + DATE_TIME_FORMAT + )} } > diff --git a/src/components/Table/TableHeader/Extensions/ExtensionModal.tsx b/src/components/Table/TableHeader/Extensions/ExtensionModal.tsx index b02f76bf7..e992bcf11 100644 --- a/src/components/Table/TableHeader/Extensions/ExtensionModal.tsx +++ b/src/components/Table/TableHeader/Extensions/ExtensionModal.tsx @@ -1,73 +1,42 @@ import { useState } from "react"; import _isEqual from "lodash/isEqual"; +import _upperFirst from "lodash/upperFirst"; import useStateRef from "react-usestateref"; import { - styled, - Button, - Checkbox, - Divider, - FormControl, - FormControlLabel, - FormGroup, - FormLabel, Grid, - IconButton, - Switch, - Stack, - Tab, TextField, + FormControlLabel, + Switch, + Stepper, + Step, + StepButton, + StepContent, Typography, + Link, } from "@mui/material"; -import TabContext from "@mui/lab/TabContext"; -import TabList from "@mui/lab/TabList"; -import TabPanel from "@mui/lab/TabPanel"; -import AddIcon from "@mui/icons-material/AddBox"; -import DeleteIcon from "@mui/icons-material/RemoveCircle"; +import ExpandIcon from "@mui/icons-material/KeyboardArrowDown"; +import InlineOpenInNewIcon from "components/InlineOpenInNewIcon"; import Modal, { IModalProps } from "components/Modal"; -import CodeEditor from "../../editors/CodeEditor"; -import CodeEditorHelper from "components/CodeEditorHelper"; +import Step1Triggers from "./Step1Triggers"; +import Step2RequiredFields from "./Step2RequiredFields"; +import Step3Conditions from "./Step3Conditions"; +import Step4Body from "./Step4Body"; import { useConfirmation } from "components/ConfirmationDialog"; -import { useProjectContext } from "contexts/ProjectContext"; -import { IExtension, triggerTypes } from "./utils"; +import { extensionNames, IExtension } from "./utils"; import { WIKI_LINKS } from "constants/externalLinks"; -const additionalVariables = [ - { - key: "change", - description: - "you can pass in field name to change.before.get() or change.after.get() to get changes", - }, - { - key: "triggerType", - description: "triggerType indicates the type of the extension invocation", - }, - { - key: "fieldTypes", - description: - "fieldTypes is a map of all fields and its corresponding field type", - }, - { - key: "extensionConfig", - description: "the configuration object of this extension", - }, -]; - -const StyledTabPanel = styled(TabPanel)({ - flexGrow: 1, - - overflowY: "auto", - margin: "0 calc(var(--dialog-spacing) * -1) 0 !important", - padding: "var(--dialog-spacing) var(--dialog-spacing) 0", - - "&[hidden]": { display: "none" }, - - display: "flex", - flexDirection: "column", -}); +type StepValidation = Record<"condition" | "extensionBody", boolean>; +export interface IExtensionModalStepProps { + extensionObject: IExtension; + setExtensionObject: React.Dispatch>; + validation: StepValidation; + setValidation: React.Dispatch>; + validationRef: React.RefObject; +} export interface IExtensionModalProps { handleClose: IModalProps["onClose"]; @@ -85,46 +54,45 @@ export default function ExtensionModal({ extensionObject: initialObject, }: IExtensionModalProps) { const { requestConfirmation } = useConfirmation(); + const [extensionObject, setExtensionObject] = useState(initialObject); - const [tab, setTab] = useState("triggersRequirements"); - const [validation, setValidation, validationRef] = useStateRef({ - condition: true, - extensionBody: true, - }); - const [, setConditionEditorActive, conditionEditorActiveRef] = - useStateRef(false); - const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false); - const { tableState } = useProjectContext(); - const columns = Object.keys(tableState?.columns ?? {}); + + const [activeStep, setActiveStep] = useState(0); + + const [validation, setValidation, validationRef] = + useStateRef({ condition: true, extensionBody: true }); + const edited = !_isEqual(initialObject, extensionObject); const handleAddOrUpdate = () => { - switch (mode) { - case "add": - handleAdd(extensionObject); - return; - case "update": - handleUpdate(extensionObject); - return; - } + if (mode === "add") handleAdd(extensionObject); + if (mode === "update") handleUpdate(extensionObject); + }; + + const stepProps = { + extensionObject, + setExtensionObject, + validation, + setValidation, + validationRef, }; return ( - + - + - setExtensionObject({ + setExtensionObject((extensionObject) => ({ ...extensionObject, active: e.target.checked, - }) + })) } size="medium" /> @@ -174,337 +142,102 @@ export default function ExtensionModal({ }activated`} /> - - - - - - setTab(val)} - variant="fullWidth" - centered - style={{ - marginTop: 0, - marginLeft: "calc(var(--dialog-spacing) * -1)", - marginRight: "calc(var(--dialog-spacing) * -1)", - }} - > - - - - - - - - - - - Triggers - - - Select a trigger that runs your extension code. Selected - actions on any cells will trigger the extension. - - - - {triggerTypes.map((trigger) => ( - { - if ( - extensionObject.triggers.includes(trigger) - ) { - setExtensionObject({ - ...extensionObject, - triggers: extensionObject.triggers.filter( - (t) => t !== trigger - ), - }); - } else { - setExtensionObject({ - ...extensionObject, - triggers: [ - ...extensionObject.triggers, - trigger, - ], - }); - } - }} - /> - } - /> - ))} - - - - - - - - Required fields (optional) - - - Optionally, select the fields that are required for the - extension to be triggered for a row. - - - *": { flexShrink: 0 }, - }} - > - {columns.sort().map((field) => ( - { - if ( - extensionObject.requiredFields.includes(field) - ) { - setExtensionObject({ - ...extensionObject, - requiredFields: - extensionObject.requiredFields.filter( - (t) => t !== field - ), - }); - } else { - setExtensionObject({ - ...extensionObject, - requiredFields: [ - ...extensionObject.requiredFields, - field, - ], - }); - } - }} - /> - } - /> - ))} + { - const isTableColumn = columns.includes(trigger); - if (isTableColumn) { - return null; - } - - return ( - - { - setExtensionObject({ - ...extensionObject, - requiredFields: - extensionObject.requiredFields.filter( - (t) => t !== trigger - ), - }); - }} - > - - - { - setExtensionObject({ - ...extensionObject, - requiredFields: - extensionObject.requiredFields.map( - (value, i) => - i === index ? event.target.value : value - ), - }); - }} - /> - - ); - })} - - - - - - - - - -
- - Conditions + "& .MuiStepLabel-root": { width: "100%" }, + "& .MuiStepLabel-label": { + display: "flex", + width: "100%", + typography: "subtitle2", + "&.Mui-active": { typography: "subtitle2" }, + }, + "& .MuiStepLabel-label svg": { + display: "block", + marginLeft: "auto", + my: ((24 - 18) / 2 / 8) * -1, + transition: (theme) => theme.transitions.create("transform"), + }, + "& .Mui-active svg": { + transform: "rotate(180deg)", + }, + }} + > + + setActiveStep(0)}> + Trigger events + + + + + Select which events trigger this extension + + + - { - setExtensionObject({ - ...extensionObject, - conditions: newValue, - }); - }} - onValideStatusUpdate={({ isValid }) => { - if (!conditionEditorActiveRef.current) { - return; - } - setValidation({ - ...validationRef.current, - condition: isValid, - }); - console.log(validationRef.current); - }} - diagnosticsOptions={{ - noSemanticValidation: false, - noSyntaxValidation: false, - noSuggestionDiagnostics: true, - }} - onMount={() => { - setConditionEditorActive(true); - }} - onUnmount={() => { - setConditionEditorActive(false); - }} - /> -
- -
+ + setActiveStep(1)}> + Required fields (optional) + + + + + Optionally, select fields that must have a value set for the + extension to be triggered for that row + + + + - -
- - Extension body + + setActiveStep(2)}> + Trigger conditions (optional) + + + + + Optionally, write a function that determines if the extension + should be triggered for a given row. Leave the function to + always return true if you do not want to write + additional logic. + + + - { - setExtensionObject({ - ...extensionObject, - extensionBody: newValue, - }); - }} - onValidStatusUpdate={({ isValid }) => { - if (!bodyEditorActiveRef.current) { - return; + + setActiveStep(3)}> + Extension body + + + + + Write the extension body function. Make sure you have set all + the required parameters.{" "} + { - setBodyEditorActive(true); - }} - onUnmount={() => { - setBodyEditorActive(false); - }} - /> -
- -
-
+ target="_blank" + rel="noopener noreferrer" + > + Docs + + +
+ + + + } actions={{ diff --git a/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx b/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx new file mode 100644 index 000000000..7e630712d --- /dev/null +++ b/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx @@ -0,0 +1,56 @@ +import { IExtensionModalStepProps } from "./ExtensionModal"; + +import { + FormControl, + FormLabel, + FormGroup, + FormControlLabel, + Checkbox, +} from "@mui/material"; + +import { triggerTypes } from "./utils"; + +export default function Step1Triggers({ + extensionObject, + setExtensionObject, +}: IExtensionModalStepProps) { + return ( + + + Triggers + + + + {triggerTypes.map((trigger) => ( + { + setExtensionObject((extensionObject) => { + if (extensionObject.triggers.includes(trigger)) { + return { + ...extensionObject, + triggers: extensionObject.triggers.filter( + (t) => t !== trigger + ), + }; + } else { + return { + ...extensionObject, + triggers: [...extensionObject.triggers, trigger], + }; + } + }); + }} + /> + } + /> + ))} + + + ); +} diff --git a/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx b/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx new file mode 100644 index 000000000..09ea596f5 --- /dev/null +++ b/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx @@ -0,0 +1,59 @@ +import { IExtensionModalStepProps } from "./ExtensionModal"; +import _sortBy from "lodash/sortBy"; + +import MultiSelect from "@rowy/multiselect"; +import { ListItemIcon } from "@mui/material"; + +import { useProjectContext } from "contexts/ProjectContext"; +import { FieldType } from "constants/fields"; +import { getFieldProp } from "components/fields"; + +export default function Step2RequiredFields({ + extensionObject, + setExtensionObject, +}: IExtensionModalStepProps) { + const { tableState } = useProjectContext(); + + return ( + c.type !== FieldType.id) + .map((c) => ({ + value: c.key, + label: c.name, + type: c.type, + })) + : [] + } + onChange={(requiredFields) => + setExtensionObject((e) => ({ ...e, requiredFields })) + } + TextFieldProps={{ autoFocus: true }} + freeText + AddButtonProps={{ children: "Add other field" }} + AddDialogProps={{ + title: "Add other field", + textFieldLabel: "Field key", + }} + itemRenderer={(option: { + value: string; + label: string; + type?: FieldType; + }) => ( + <> + + {option.type && getFieldProp("icon", option.type)} + + {option.label} + {option.value} + + )} + /> + ); +} diff --git a/src/components/Table/TableHeader/Extensions/Step3Conditions.tsx b/src/components/Table/TableHeader/Extensions/Step3Conditions.tsx new file mode 100644 index 000000000..ad23b46bb --- /dev/null +++ b/src/components/Table/TableHeader/Extensions/Step3Conditions.tsx @@ -0,0 +1,71 @@ +import { IExtensionModalStepProps } from "./ExtensionModal"; +import useStateRef from "react-usestateref"; + +import CodeEditor from "components/Table/editors/CodeEditor"; +import CodeEditorHelper from "components/CodeEditorHelper"; + +import { WIKI_LINKS } from "constants/externalLinks"; + +const additionalVariables = [ + { + key: "change", + description: + "you can pass in field name to change.before.get() or change.after.get() to get changes", + }, + { + key: "triggerType", + description: "triggerType indicates the type of the extension invocation", + }, + { + key: "fieldTypes", + description: + "fieldTypes is a map of all fields and its corresponding field type", + }, + { + key: "extensionConfig", + description: "the configuration object of this extension", + }, +]; + +export default function Step3Conditions({ + extensionObject, + setExtensionObject, + setValidation, + validationRef, +}: IExtensionModalStepProps) { + const [, setConditionEditorActive, conditionEditorActiveRef] = + useStateRef(false); + + return ( + <> +
+ { + setExtensionObject({ + ...extensionObject, + conditions: newValue, + }); + }} + onValidStatusUpdate={({ isValid }) => { + if (!conditionEditorActiveRef.current) return; + setValidation({ ...validationRef.current!, condition: isValid }); + }} + diagnosticsOptions={{ + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: true, + }} + onMount={() => setConditionEditorActive(true)} + onUnmount={() => setConditionEditorActive(false)} + /> +
+ + + + ); +} diff --git a/src/components/Table/TableHeader/Extensions/Step4Body.tsx b/src/components/Table/TableHeader/Extensions/Step4Body.tsx new file mode 100644 index 000000000..60060d6c3 --- /dev/null +++ b/src/components/Table/TableHeader/Extensions/Step4Body.tsx @@ -0,0 +1,77 @@ +import { IExtensionModalStepProps } from "./ExtensionModal"; +import _upperFirst from "lodash/upperFirst"; +import useStateRef from "react-usestateref"; + +import CodeEditor from "components/Table/editors/CodeEditor"; +import CodeEditorHelper from "components/CodeEditorHelper"; + +import { WIKI_LINKS } from "constants/externalLinks"; + +const additionalVariables = [ + { + key: "change", + description: + "you can pass in field name to change.before.get() or change.after.get() to get changes", + }, + { + key: "triggerType", + description: "triggerType indicates the type of the extension invocation", + }, + { + key: "fieldTypes", + description: + "fieldTypes is a map of all fields and its corresponding field type", + }, + { + key: "extensionConfig", + description: "the configuration object of this extension", + }, +]; + +export default function Step4Body({ + extensionObject, + setExtensionObject, + setValidation, + validationRef, +}: IExtensionModalStepProps) { + const [, setBodyEditorActive, bodyEditorActiveRef] = useStateRef(false); + + return ( + <> +
+ { + setExtensionObject({ + ...extensionObject, + extensionBody: newValue, + }); + }} + onValidStatusUpdate={({ isValid }) => { + if (!bodyEditorActiveRef.current) return; + setValidation({ + ...validationRef.current!, + extensionBody: isValid, + }); + }} + diagnosticsOptions={{ + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: true, + }} + onMount={() => setBodyEditorActive(true)} + onUnmount={() => setBodyEditorActive(false)} + /> +
+ + + + ); +} diff --git a/src/components/Table/TableHeader/Extensions/index.tsx b/src/components/Table/TableHeader/Extensions/index.tsx index 247edc89c..293cbff34 100644 --- a/src/components/Table/TableHeader/Extensions/index.tsx +++ b/src/components/Table/TableHeader/Extensions/index.tsx @@ -1,9 +1,7 @@ import { useState } from "react"; import _isEqual from "lodash/isEqual"; -import { db } from "../../../../firebase"; -import { useSnackbar } from "notistack"; -import { Breadcrumbs, Typography, Button } from "@mui/material"; +import { Breadcrumbs } from "@mui/material"; import TableHeaderButton from "../TableHeaderButton"; import ExtensionIcon from "assets/icons/Extension"; @@ -17,20 +15,19 @@ import { useAppContext } from "contexts/AppContext"; import { useConfirmation } from "components/ConfirmationDialog"; import { useSnackLogContext } from "contexts/SnackLogContext"; -import { emptyExtensionObject, IExtension, IExtensionType } from "./utils"; -import { name } from "@root/package.json"; +import { emptyExtensionObject, IExtension, ExtensionType } from "./utils"; import { runRoutes } from "constants/runRoutes"; import { analytics } from "@src/analytics"; -export default function ExtensionsEditor() { - const { enqueueSnackbar } = useSnackbar(); +export default function Extensions() { const { tableState, tableActions, rowyRun } = useProjectContext(); const appContext = useAppContext(); const { requestConfirmation } = useConfirmation(); - const currentextensionObjects = (tableState?.config.extensionObjects ?? + + const currentExtensionObjects = (tableState?.config.extensionObjects ?? []) as IExtension[]; const [localExtensionsObjects, setLocalExtensionsObjects] = useState( - currentextensionObjects + currentExtensionObjects ); const [openExtensionList, setOpenExtensionList] = useState(false); const [openMigrationGuide, setOpenMigrationGuide] = useState(false); @@ -39,8 +36,9 @@ export default function ExtensionsEditor() { extensionObject: IExtension; index?: number; } | null>(null); + const snackLogContext = useSnackLogContext(); - const edited = !_isEqual(currentextensionObjects, localExtensionsObjects); + const edited = !_isEqual(currentExtensionObjects, localExtensionsObjects); const tablePathTokens = tableState?.tablePath?.split("/").filter(function (_, i) { @@ -65,7 +63,7 @@ export default function ExtensionsEditor() { body: "You will lose changes you have made to extensions", confirm: "Discard", handleConfirm: () => { - setLocalExtensionsObjects(currentextensionObjects); + setLocalExtensionsObjects(currentExtensionObjects); setOpenExtensionList(false); }, }); @@ -199,13 +197,13 @@ export default function ExtensionsEditor() { children={ <> - {tablePathTokens.map((pathToken) => { - return {pathToken}; - })} + {tablePathTokens.map((pathToken) => ( + {pathToken} + ))} { + handleAddExtension={(type: ExtensionType) => { setExtensionModal({ mode: "add", extensionObject: emptyExtensionObject( diff --git a/src/components/Table/TableHeader/Extensions/utils.ts b/src/components/Table/TableHeader/Extensions/utils.ts index a57870a0a..61366df40 100644 --- a/src/components/Table/TableHeader/Extensions/utils.ts +++ b/src/components/Table/TableHeader/Extensions/utils.ts @@ -1,51 +1,54 @@ -type IExtensionType = - | "task" - | "docSync" - | "historySnapshot" - | "algoliaIndex" - | "meiliIndex" - | "bigqueryIndex" - | "slackMessage" - | "sendgridEmail" - | "apiCall" - | "twilioMessage"; +export const extensionTypes = [ + "task", + "docSync", + "historySnapshot", + "algoliaIndex", + "meiliIndex", + "bigqueryIndex", + "slackMessage", + "sendgridEmail", + "apiCall", + "twilioMessage", +] as const; + +export type ExtensionType = typeof extensionTypes[number]; + +export const extensionNames: Record = { + task: "Task", + docSync: "Doc Sync", + historySnapshot: "History Snapshot", + algoliaIndex: "Algolia Index", + meiliIndex: "MeiliSearch Index", + bigqueryIndex: "Big Query Index", + slackMessage: "Slack Message", + sendgridEmail: "SendGrid Email", + apiCall: "API Call", + twilioMessage: "Twilio Message", +}; -type IExtensionTrigger = "create" | "update" | "delete"; +export type ExtensionTrigger = "create" | "update" | "delete"; -interface IExtensionEditor { +export interface IExtensionEditor { displayName: string; photoURL: string; lastUpdate: number; } -interface IExtension { +export interface IExtension { // rowy meta fields name: string; active: boolean; lastEditor: IExtensionEditor; // ft build fields - triggers: IExtensionTrigger[]; - type: IExtensionType; + triggers: ExtensionTrigger[]; + type: ExtensionType; requiredFields: string[]; extensionBody: string; conditions: string; } -const triggerTypes: IExtensionTrigger[] = ["create", "update", "delete"]; - -const extensionTypes: IExtensionType[] = [ - "task", - "docSync", - "historySnapshot", - "algoliaIndex", - "meiliIndex", - "bigqueryIndex", - "slackMessage", - "sendgridEmail", - "apiCall", - "twilioMessage", -]; +export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { task: `const extensionBody: TaskBody = async({row, db, change, ref}) => { @@ -158,8 +161,8 @@ const extensionBodyTemplate = { }`, }; -function emptyExtensionObject( - type: IExtensionType, +export function emptyExtensionObject( + type: ExtensionType, user: IExtensionEditor ): IExtension { return { @@ -176,7 +179,7 @@ function emptyExtensionObject( lastEditor: user, }; } -function sparkToExtensionObjects( +export function sparkToExtensionObjects( sparkConfig: string, user: IExtensionEditor ): IExtension[] { @@ -225,8 +228,8 @@ function sparkToExtensionObjects( lastEditor: user, // ft build fields - triggers: (spark.triggers ?? []) as IExtensionTrigger[], - type: spark.type as IExtensionType, + triggers: (spark.triggers ?? []) as ExtensionTrigger[], + type: spark.type as ExtensionType, requiredFields: spark.requiredFields ?? [], extensionBody: spark.sparkBody, conditions: spark.shouldRun ?? "", @@ -234,11 +237,3 @@ function sparkToExtensionObjects( }); return extensionObjects ?? []; } - -export { - extensionTypes, - triggerTypes, - emptyExtensionObject, - sparkToExtensionObjects, -}; -export type { IExtension, IExtensionType, IExtensionEditor }; diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 72e01620f..644907bc3 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -91,6 +91,16 @@ export const components = (theme: Theme): ThemeOptions => { "& input, & label": theme.typography.body2, }, + + ".visually-hidden": { + position: "absolute", + clip: "rect(1px, 1px, 1px, 1px)", + overflow: "hidden", + height: 1, + width: 1, + padding: 0, + border: 0, + }, }, }, From f7d0be64f73a0591743ee17bb07216fd0f6085bf Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 14 Oct 2021 23:51:37 +1100 Subject: [PATCH 59/64] add extensionsDocSync WIKI_LINK --- src/constants/externalLinks.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constants/externalLinks.ts b/src/constants/externalLinks.ts index c65dad2ed..43d1e2d6e 100644 --- a/src/constants/externalLinks.ts +++ b/src/constants/externalLinks.ts @@ -42,10 +42,11 @@ const WIKI_PATHS = { rowyRun: "/rowy-run", extensions: "/extensions", + extensionsDocSync: "/extensions/doc-sync", extensionsAlgoliaIndex: "/extensions/algolia-index", + extensionsSlackMessage: "/extensions/slack-message", extensionsSendgridEmail: "/extensions/sendgrid-email", extensionsTwilioMessage: "/extensions/twilio-message", - extensionsSlackMessage: "/extensions/slack-message", }; export const WIKI_LINKS = _mapValues( WIKI_PATHS, From cd9798c636f9b10c650016f87f3f2afff295a6df Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 15 Oct 2021 00:00:21 +1100 Subject: [PATCH 60/64] Allow users to add custom options in Single and Multi Select fields --- .../fields/SingleSelect/Settings.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index b34a7dd37..d744bcf22 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -8,12 +8,13 @@ import { IconButton, Typography, Divider, + FormControlLabel, + Checkbox, + FormHelperText, } from "@mui/material"; import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; -import Subheading from "components/Table/ColumnMenu/Subheading"; - const useStyles = makeStyles(() => createStyles({ field: { @@ -106,6 +107,25 @@ export default function Settings({ handleChange, config }) { /> + + handleChange("freeText")(e.target.checked)} + /> + } + label={ + <> + Users can add custom options + + Custom options will only appear in the row it was added to. They + will not appear in the list of options above. + + + } + style={{ marginLeft: -10 }} + />
); } From faa82262b59e04fa02346175b62b3d9891baaaa2 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 15 Oct 2021 15:28:34 +1100 Subject: [PATCH 61/64] add ellipsis to multiselect add new button to signify additional user input is required --- package.json | 2 +- .../Table/TableHeader/Extensions/Step2RequiredFields.tsx | 2 +- src/components/TableSettings/form.tsx | 4 +--- src/components/Wizards/ImportCsvWizard/Step1Columns.tsx | 2 +- yarn.lock | 8 ++++---- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 317a012fc..dfd52b0a8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", "@rowy/form-builder": "^0.3.1", - "@rowy/multiselect": "^0.2.1", + "@rowy/multiselect": "^0.2.3", "@tinymce/tinymce-react": "^3.12.6", "algoliasearch": "^4.8.6", "ansi-to-react": "^6.1.5", diff --git a/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx b/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx index 09ea596f5..373487ccc 100644 --- a/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx +++ b/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx @@ -36,7 +36,7 @@ export default function Step2RequiredFields({ } TextFieldProps={{ autoFocus: true }} freeText - AddButtonProps={{ children: "Add other field" }} + AddButtonProps={{ children: "Add other field…" }} AddDialogProps={{ title: "Add other field", textFieldLabel: "Field key", diff --git a/src/components/TableSettings/form.tsx b/src/components/TableSettings/form.tsx index 791fa8100..289782261 100644 --- a/src/components/TableSettings/form.tsx +++ b/src/components/TableSettings/form.tsx @@ -73,9 +73,7 @@ export const tableSettings = ( ), - AddButtonProps: { - children: "Add collection", - }, + AddButtonProps: { children: "Add collection…" }, AddDialogProps: { title: "Add collection", textFieldLabel: ( diff --git a/src/components/Wizards/ImportCsvWizard/Step1Columns.tsx b/src/components/Wizards/ImportCsvWizard/Step1Columns.tsx index 2647506d3..9b8bf433f 100644 --- a/src/components/Wizards/ImportCsvWizard/Step1Columns.tsx +++ b/src/components/Wizards/ImportCsvWizard/Step1Columns.tsx @@ -282,7 +282,7 @@ export default function Step1Columns({ displayEmpty labelPlural="columns" freeText - AddButtonProps={{ children: "Add new column" }} + AddButtonProps={{ children: "Add new column…" }} AddDialogProps={{ title: "Add new column", textFieldLabel: "Column name", diff --git a/yarn.lock b/yarn.lock index 6ac289de1..529689abc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2679,10 +2679,10 @@ use-debounce "^3.4.3" yup "^0.32.9" -"@rowy/multiselect@^0.2.1": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@rowy/multiselect/-/multiselect-0.2.2.tgz#5dc0d139c1c70624b2bc3a3da4b390b4d7c9afd8" - integrity sha512-gpFCPyC6c3KrfnyyF9oMPuR4l7N8Fsfh2WpHJb00e83zmYxzHXSK/xaU3L2Itg89kCfsFlJwXcHc/T1+ipTRzA== +"@rowy/multiselect@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@rowy/multiselect/-/multiselect-0.2.3.tgz#caf5ee769a6c3ce5a4124120d38820caf1f411a1" + integrity sha512-FiESN3VE2Rz++y6cZXzkA4RJmRZ0r3/1Hy6oAfJtroJ/H/r0xjJYGA5qwpmQKNI08W0KCfkBC7JM0OtRiD3GVA== "@sindresorhus/is@^0.14.0": version "0.14.0" From 15d0fd804d222bf01d915d7c30a0afd1770e3e31 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 15 Oct 2021 17:38:37 +1100 Subject: [PATCH 62/64] fix SuggestedRules causing horizontal scroll --- src/components/TableSettings/SuggestedRules.tsx | 4 +++- src/components/TableSettings/index.tsx | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/TableSettings/SuggestedRules.tsx b/src/components/TableSettings/SuggestedRules.tsx index d2bbe7423..89c10494d 100644 --- a/src/components/TableSettings/SuggestedRules.tsx +++ b/src/components/TableSettings/SuggestedRules.tsx @@ -62,7 +62,9 @@ export default function SuggestedRules({ return ( <> {label} -
{generatedRules}
+
+        {generatedRules}
+      
diff --git a/src/components/TableSettings/index.tsx b/src/components/TableSettings/index.tsx index 606172f9b..0809616ac 100644 --- a/src/components/TableSettings/index.tsx +++ b/src/components/TableSettings/index.tsx @@ -53,15 +53,14 @@ export default function TableSettingsDialog({ const handleSubmit = async (v) => { const { _suggestedRules, ...values } = v; - const data: any = { - ...values, - }; + const data: any = { ...values }; if (values.schemaSource) data.schemaSource = _find(tables, { id: values.schemaSource }); if (mode === TableSettingsDialogModes.update) { - await Promise.all([settingsActions?.updateTable(data), clearDialog()]); + await settingsActions?.updateTable(data); + clearDialog(); } else { settingsActions?.createTable(data); @@ -77,9 +76,7 @@ export default function TableSettingsDialog({ } analytics.logEvent( TableSettingsDialogModes.update ? "update_table" : "create_table", - { - type: values.tableType, - } + { type: values.tableType } ); clearDialog(); }; From 0fd3d884e3275d00ebbe61347c7f873db9f7c569 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 15 Oct 2021 17:38:47 +1100 Subject: [PATCH 63/64] add audit fields --- package.json | 2 +- src/assets/icons/CreatedAt.tsx | 10 +++ src/assets/icons/UpdatedAt.tsx | 9 ++ src/components/Table/ColumnMenu/NewColumn.tsx | 72 +++++++++++++-- src/components/TableSettings/form.tsx | 65 +++++++++++++- .../fields/CreatedAt/SideDrawerField.tsx | 34 +++++++ src/components/fields/CreatedAt/TableCell.tsx | 20 +++++ src/components/fields/CreatedAt/index.tsx | 36 ++++++++ src/components/fields/CreatedBy/Settings.tsx | 46 ++++++++++ .../fields/CreatedBy/SideDrawerField.tsx | 81 ++++++++--------- src/components/fields/CreatedBy/TableCell.tsx | 24 +++-- src/components/fields/CreatedBy/index.tsx | 8 +- src/components/fields/Date/Settings.tsx | 2 +- src/components/fields/DateTime/Settings.tsx | 2 +- .../fields/UpdatedAt/SideDrawerField.tsx | 34 +++++++ src/components/fields/UpdatedAt/TableCell.tsx | 20 +++++ src/components/fields/UpdatedAt/index.tsx | 37 ++++++++ .../fields/UpdatedBy/SideDrawerField.tsx | 88 ++++++++++--------- src/components/fields/UpdatedBy/TableCell.tsx | 32 ++++--- src/components/fields/UpdatedBy/index.tsx | 9 +- .../fields/User/SideDrawerField.tsx | 2 +- src/components/fields/User/TableCell.tsx | 20 +++-- src/components/fields/User/index.tsx | 5 ++ src/components/fields/index.tsx | 13 ++- src/constants/fields.ts | 7 +- src/contexts/ProjectContext.tsx | 52 +++++------ src/hooks/useSettings.ts | 22 +++-- yarn.lock | 5 ++ 28 files changed, 585 insertions(+), 172 deletions(-) create mode 100644 src/assets/icons/CreatedAt.tsx create mode 100644 src/assets/icons/UpdatedAt.tsx create mode 100644 src/components/fields/CreatedAt/SideDrawerField.tsx create mode 100644 src/components/fields/CreatedAt/TableCell.tsx create mode 100644 src/components/fields/CreatedAt/index.tsx create mode 100644 src/components/fields/CreatedBy/Settings.tsx create mode 100644 src/components/fields/UpdatedAt/SideDrawerField.tsx create mode 100644 src/components/fields/UpdatedAt/TableCell.tsx create mode 100644 src/components/fields/UpdatedAt/index.tsx diff --git a/package.json b/package.json index dfd52b0a8..946ca2f7c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assets/icons/CreatedAt.tsx b/src/assets/icons/CreatedAt.tsx new file mode 100644 index 000000000..1bd99e8ce --- /dev/null +++ b/src/assets/icons/CreatedAt.tsx @@ -0,0 +1,10 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiClockPlusOutline } from "@mdi/js"; + +export default function CreatedAt(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/assets/icons/UpdatedAt.tsx b/src/assets/icons/UpdatedAt.tsx new file mode 100644 index 000000000..754a89ca4 --- /dev/null +++ b/src/assets/icons/UpdatedAt.tsx @@ -0,0 +1,9 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; + +export default function UpdatedAt(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/components/Table/ColumnMenu/NewColumn.tsx b/src/components/Table/ColumnMenu/NewColumn.tsx index 78ea9b7c6..eb4c0e544 100644 --- a/src/components/Table/ColumnMenu/NewColumn.tsx +++ b/src/components/Table/ColumnMenu/NewColumn.tsx @@ -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; @@ -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); + const isAuditField = + type === FieldType.createdBy || + type === FieldType.createdAt || + type === FieldType.updatedBy || + type === FieldType.updatedAt; + useEffect(() => { - if (type !== FieldType.id) setFieldKey(_camel(columnLabel)); - }, [columnLabel]); + if (type !== FieldType.id && !isAuditField) + setFieldKey(_camel(columnLabel)); + }, [columnLabel, type, isAuditField]); useEffect(() => { - if (type === FieldType.id) { - setColumnLabel("ID"); - setFieldKey("id"); + 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" } }} />
+ + {isAuditField && table?.audit === false && ( +
+ + This field requires auditing to be enabled on this table. + + + +
+ )} } actions={{ diff --git a/src/components/TableSettings/form.tsx b/src/components/TableSettings/form.tsx index 289782261..5d414c86d 100644 --- a/src/components/TableSettings/form.tsx +++ b/src/components/TableSettings/form.tsx @@ -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", + sx: { "&&": { mb: 1 }, typography: "button", ml: 2 / 8 }, + } + : null, + mode === TableSettingsDialogModes.create + ? { + type: FieldType.checkbox, + name: `_initialColumns.${TableFieldType.createdBy}`, + 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: Automatically log who updates a row", + label: "Updated By", + displayCondition: "return values.audit", + gridCols: 6, + disablePaddingTop: true, } : null, mode === TableSettingsDialogModes.create ? { type: FieldType.checkbox, - name: `_initialColumns.${TableFieldType.createdBy}`, - label: "Created By: Automatically log who creates a row", + 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, diff --git a/src/components/fields/CreatedAt/SideDrawerField.tsx b/src/components/fields/CreatedAt/SideDrawerField.tsx new file mode 100644 index 000000000..f71564a08 --- /dev/null +++ b/src/components/fields/CreatedAt/SideDrawerField.tsx @@ -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
; + + const dateLabel = format( + value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, + column.config?.format || DATE_TIME_FORMAT + ); + + return ( +
+ {dateLabel} +
+ ); +} diff --git a/src/components/fields/CreatedAt/TableCell.tsx b/src/components/fields/CreatedAt/TableCell.tsx new file mode 100644 index 000000000..5955a8dc9 --- /dev/null +++ b/src/components/fields/CreatedAt/TableCell.tsx @@ -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 ( + {dateLabel} + ); +} diff --git a/src/components/fields/CreatedAt/index.tsx b/src/components/fields/CreatedAt/index.tsx new file mode 100644 index 000000000..19d752f16 --- /dev/null +++ b/src/components/fields/CreatedAt/index.tsx @@ -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: , + 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; diff --git a/src/components/fields/CreatedBy/Settings.tsx b/src/components/fields/CreatedBy/Settings.tsx new file mode 100644 index 000000000..072d55939 --- /dev/null +++ b/src/components/fields/CreatedBy/Settings.tsx @@ -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 ( + <> + ( + {option.label} + )} + label="Display format" + multiple={false} + freeText + clearable={false} + searchable={false} + value={config.format ?? DATE_TIME_FORMAT} + onChange={handleChange("format")} + TextFieldProps={{ + helperText: ( + + Date format reference + + + ), + }} + /> + + ); +} diff --git a/src/components/fields/CreatedBy/SideDrawerField.tsx b/src/components/fields/CreatedBy/SideDrawerField.tsx index 587465222..bbad9cc3b 100644 --- a/src/components/fields/CreatedBy/SideDrawerField.tsx +++ b/src/components/fields/CreatedBy/SideDrawerField.tsx @@ -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(); + const { table } = useProjectContext(); + const value = useWatch({ + control, + name: table?.auditFieldCreatedBy || "_createdBy", + }); + + if (!value || !value.displayName || !value.timestamp) + return
; + + const dateLabel = format( + value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, + column.config?.format || DATE_TIME_FORMAT + ); + return ( - { - if (!value || !value.displayName || !value.timestamp) - return
; - const dateLabel = format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - DATE_TIME_FORMAT - ); - return ( - - - - - {value.displayName} ({value.email}) - - Created at {dateLabel} - - - - ); - }} - /> + + + + + {value.displayName} ({value.email}) + + Created at {dateLabel} + + + ); } diff --git a/src/components/fields/CreatedBy/TableCell.tsx b/src/components/fields/CreatedBy/TableCell.tsx index e760867bd..180662cf8 100644 --- a/src/components/fields/CreatedBy/TableCell.tsx +++ b/src/components/fields/CreatedBy/TableCell.tsx @@ -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 ( - } - label={value.displayName} - sx={{ mx: -0.25, height: 24 }} - /> + + + {value.displayName} + ); } diff --git a/src/components/fields/CreatedBy/index.tsx b/src/components/fields/CreatedBy/index.tsx index 7c2ff5bf5..d0ee82313 100644 --- a/src/components/fields/CreatedBy/index.tsx +++ b/src/components/fields/CreatedBy/index.tsx @@ -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: , 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; diff --git a/src/components/fields/Date/Settings.tsx b/src/components/fields/Date/Settings.tsx index 201d2722f..209426d7f 100644 --- a/src/components/fields/Date/Settings.tsx +++ b/src/components/fields/Date/Settings.tsx @@ -15,7 +15,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) { itemRenderer={(option) => ( {option.label} )} - label="Format" + label="Display format" multiple={false} freeText clearable={false} diff --git a/src/components/fields/DateTime/Settings.tsx b/src/components/fields/DateTime/Settings.tsx index aaa722199..072d55939 100644 --- a/src/components/fields/DateTime/Settings.tsx +++ b/src/components/fields/DateTime/Settings.tsx @@ -21,7 +21,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) { itemRenderer={(option) => ( {option.label} )} - label="Format" + label="Display format" multiple={false} freeText clearable={false} diff --git a/src/components/fields/UpdatedAt/SideDrawerField.tsx b/src/components/fields/UpdatedAt/SideDrawerField.tsx new file mode 100644 index 000000000..7fe9dcb5a --- /dev/null +++ b/src/components/fields/UpdatedAt/SideDrawerField.tsx @@ -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
; + + const dateLabel = format( + value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, + column.config?.format || DATE_TIME_FORMAT + ); + + return ( +
+ {dateLabel} +
+ ); +} diff --git a/src/components/fields/UpdatedAt/TableCell.tsx b/src/components/fields/UpdatedAt/TableCell.tsx new file mode 100644 index 000000000..008f07f81 --- /dev/null +++ b/src/components/fields/UpdatedAt/TableCell.tsx @@ -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 ( + {dateLabel} + ); +} diff --git a/src/components/fields/UpdatedAt/index.tsx b/src/components/fields/UpdatedAt/index.tsx new file mode 100644 index 000000000..e9206321f --- /dev/null +++ b/src/components/fields/UpdatedAt/index.tsx @@ -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: , + 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; diff --git a/src/components/fields/UpdatedBy/SideDrawerField.tsx b/src/components/fields/UpdatedBy/SideDrawerField.tsx index f392b99a1..cef0ebbb9 100644 --- a/src/components/fields/UpdatedBy/SideDrawerField.tsx +++ b/src/components/fields/UpdatedBy/SideDrawerField.tsx @@ -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(); + const { table } = useProjectContext(); + const value = useWatch({ + control, + name: table?.auditFieldUpdatedBy || "_updatedBy", + }); + + if (!value || !value.displayName || !value.timestamp) + return
; + + const dateLabel = format( + value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, + column.config?.format || DATE_TIME_FORMAT + ); + return ( - { - if (!value || !value.displayName || !value.timestamp) - return
; - const dateLabel = format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - DATE_TIME_FORMAT - ); - return ( - - - - - {value.displayName} ({value.email}) - - Updated field {value.updatedField} at {dateLabel} - - - - ); - }} - /> + + + + + {value.displayName} ({value.email}) + + Updated + {value.updatedField && ( + <> + {" "} + field {value.updatedField} + + )}{" "} + at {dateLabel} + + + ); } diff --git a/src/components/fields/UpdatedBy/TableCell.tsx b/src/components/fields/UpdatedBy/TableCell.tsx index c8c8c389a..d143ef184 100644 --- a/src/components/fields/UpdatedBy/TableCell.tsx +++ b/src/components/fields/UpdatedBy/TableCell.tsx @@ -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 ( - Updated field {value.updatedField} + Updated + {value.updatedField && ( + <> + {" "} + field {value.updatedField} + + )}
at {dateLabel} } > - } - label={value.displayName} - sx={{ mx: -0.25, height: 24 }} - /> + + + {value.displayName} +
); } diff --git a/src/components/fields/UpdatedBy/index.tsx b/src/components/fields/UpdatedBy/index.tsx index cd130fbb5..8925730b5 100644 --- a/src/components/fields/UpdatedBy/index.tsx +++ b/src/components/fields/UpdatedBy/index.tsx @@ -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: , 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; diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index f6677c7f3..025078c0a 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -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; diff --git a/src/components/fields/User/TableCell.tsx b/src/components/fields/User/TableCell.tsx index 70be7d43c..f76320c4f 100644 --- a/src/components/fields/User/TableCell.tsx +++ b/src/components/fields/User/TableCell.tsx @@ -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 = ( - } - label={value.displayName} - sx={{ mx: -0.25, height: 24 }} - /> + + + {value.displayName} + ); 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 {chip}; diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx index bd3810ef4..4c34ac034 100644 --- a/src/components/fields/User/index.tsx +++ b/src/components/fields/User/index.tsx @@ -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; diff --git a/src/components/fields/index.tsx b/src/components/fields/index.tsx index 684a88091..fff4d32e7 100644 --- a/src/components/fields/index.tsx +++ b/src/components/fields/index.tsx @@ -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, ]; diff --git a/src/constants/fields.ts b/src/constants/fields.ts index 56a1ab871..991a34f02 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -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", } diff --git a/src/contexts/ProjectContext.tsx b/src/contexts/ProjectContext.tsx index b631462f9..013fb4e5e 100644 --- a/src/contexts/ProjectContext.tsx +++ b/src/contexts/ProjectContext.tsx @@ -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; deleteTable: (id: string) => void; }; @@ -92,6 +97,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => { const { tableState, tableActions } = useTable(); const [tables, setTables] = useState(); 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, diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 6a655b3d0..298dce3af 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -45,7 +45,7 @@ export default function useSettings() { const tableSchemaDocRef = db.doc(tableSchemaPath); // Get columns from schemaSource if provided - let columns: Record = []; + let columns: Record = {}; 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] : []; diff --git a/yarn.lock b/yarn.lock index 529689abc..5c7be15ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From c0a810ebd2e2ef2f31bdb710c05cb4bf0d1a3b7d Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 15 Oct 2021 17:43:35 +1100 Subject: [PATCH 64/64] bump version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 946ca2f7c..b30884a22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rowy", - "version": "2.0.0", + "version": "2.1.0", "homepage": "https://rowy.io", "repository": { "type": "git",