From d80815f1d837438bbb073b594ef4191ca536a4c6 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 3 Jan 2023 09:47:59 +0300 Subject: [PATCH 01/91] feat(ROWY-831): add preview table --- .../fields/Formula/PreviewTable.tsx | 74 +++++++++++++++++++ src/components/fields/Formula/Settings.tsx | 38 +++++++++- .../fields/Formula/TableSourcePreview.ts | 70 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/components/fields/Formula/PreviewTable.tsx create mode 100644 src/components/fields/Formula/TableSourcePreview.ts diff --git a/src/components/fields/Formula/PreviewTable.tsx b/src/components/fields/Formula/PreviewTable.tsx new file mode 100644 index 00000000..66113b32 --- /dev/null +++ b/src/components/fields/Formula/PreviewTable.tsx @@ -0,0 +1,74 @@ +import { Provider, useAtom } from "jotai"; + +import { currentUserAtom } from "@src/atoms/projectScope"; +import { + tableRowsDbAtom, + tableScope, + tableSettingsAtom, +} from "@src/atoms/tableScope"; + +import TablePage from "@src/pages/Table/TablePage"; +import { TableSchema } from "@src/types/table"; +import { Box, InputLabel } from "@mui/material"; +import TableSourcePreview from "./TableSourcePreview"; + +const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { + const [currentUser] = useAtom(currentUserAtom, tableScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + return ( + + Preview table + + + div:first-child > *:not(:first-child)": { + display: "none", + }, + // table grid + "& > div:nth-child(2)": { + height: "unset", + }, + // emtpy state + "& .empty-state": { + display: "none", + }, + // column actions - add column + '& [data-col-id="_rowy_column_actions"]': { + padding: 0, + width: 0, + }, + '& [data-col-id="_rowy_column_actions"] > button': { + display: "none", + }, + }} + > + + + + + ); +}; + +export default PreviewTable; diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index ad0eb16f..5a0ac101 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { useDebouncedCallback } from "use-debounce"; import { useAtom } from "jotai"; import MultiSelect from "@rowy/multiselect"; @@ -12,12 +12,19 @@ import { Tooltip, } from "@mui/material"; +import { + tableColumnsOrderedAtom, + tableSchemaAtom, + tableScope, +} from "@src/atoms/tableScope"; + import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; import { ISettingsProps } from "@src/components/fields/types"; -import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; +import { ColumnConfig } from "@src/types/table"; import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; +import PreviewTable from "./PreviewTable"; import { getFieldProp } from ".."; /* eslint-disable import/no-webpack-loader-syntax */ @@ -36,14 +43,40 @@ const diagnosticsOptions = { export default function Settings({ config, + fieldName, onChange, onBlur, errors, }: ISettingsProps) { + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const returnType = getFieldProp("dataType", config.renderFieldType) ?? "any"; const formulaFn = config?.formulaFn ? config.formulaFn : defaultFn; + const previewTableSchema = useMemo(() => { + const columns = tableSchema.columns || {}; + return { + ...tableSchema, + columns: Object.keys(columns).reduce((previewSchema, key) => { + if ((config.listenerFields || []).includes(columns[key].fieldName)) { + previewSchema[key] = { + ...columns[key], + fixed: false, + width: undefined, + }; + } + if (columns[key].fieldName === fieldName) { + previewSchema[key] = { + ...columns[key], + config, + fixed: true, + }; + } + return previewSchema; + }, {} as { [key: string]: ColumnConfig }), + }; + }, [config, fieldName, tableSchema]); + return ( @@ -133,6 +166,7 @@ export default function Settings({ /> + ); } diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts new file mode 100644 index 00000000..57deb3d9 --- /dev/null +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -0,0 +1,70 @@ +import { useCallback } from "react"; +import { useSetAtom } from "jotai"; +import { useAtomCallback } from "jotai/utils"; +import { cloneDeep, findIndex, sortBy } from "lodash-es"; + +import { + _deleteRowDbAtom, + _updateRowDbAtom, + tableNextPageAtom, + tableRowsDbAtom, + tableSchemaAtom, + tableScope, +} from "@src/atoms/tableScope"; + +import { TableRow, TableSchema } from "@src/types/table"; +import { updateRowData } from "@src/utils/table"; + +const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { + const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope); + setTableSchemaAtom(() => ({ + ...tableSchema, + _rowy_ref: "preview", + })); + + const setRows = useSetAtom(tableRowsDbAtom, tableScope); + const readRowsDb = useAtomCallback( + useCallback((get) => get(tableRowsDbAtom), []), + tableScope + ); + + const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope); + setUpdateRowDb(() => async (path: string, update: Partial) => { + const rows = await readRowsDb(); + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index === -1) { + setRows( + sortBy( + [ + ...rows, + { ...update, _rowy_ref: { id: path.split("/").pop()!, path } }, + ], + ["_rowy_ref.id"] + ) + ); + } else { + const updatedRows = [...rows]; + updatedRows[index] = cloneDeep(rows[index]); + updatedRows[index] = updateRowData(updatedRows[index], update); + setRows(updatedRows); + } + return Promise.resolve(); + }); + + const setDeleteRowDb = useSetAtom(_deleteRowDbAtom, tableScope); + setDeleteRowDb(() => async (path: string) => { + const rows = await readRowsDb(); + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index > -1) { + setRows(rows.filter((_, idx) => idx !== index)); + } + return Promise.resolve(); + }); + + const setNextPageAtom = useSetAtom(tableNextPageAtom, tableScope); + setNextPageAtom({ loading: false, available: false }); + + return null; +}; + +export default TableSourcePreview; From ff0fcddbf5ed3fc4579860ed972c8f2e94f8a0bb Mon Sep 17 00:00:00 2001 From: Harini Janakiraman Date: Mon, 23 Jan 2023 18:21:30 +1100 Subject: [PATCH 02/91] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13e8317b..4e22728f 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Alternatively, you can manually install by ## Roadmap -[View our roadmap](https://demo.rowy.io/table/roadmap) on Rowy - Upvote, +[View our roadmap](https://roadmap.rowy.io/) on Rowy - Upvote, downvote, share your thoughts! If you'd like to propose a feature, submit an issue @@ -129,5 +129,5 @@ If you'd like to propose a feature, submit an issue ## Help -- Live chat support on [Discord](https://discord.gg/fjBugmvzZP) +- Live chat support on [Discord](https://www.rowy.io/discord) - [Email](mailto:hello@rowy.io) From d4f2c707468f45f85fb19d2f5021b19cb34c7f01 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:56:49 +0000 Subject: [PATCH 03/91] editable user column --- src/components/fields/User/DisplayCell.tsx | 118 ++++++++++--- src/components/fields/User/EditorCell.tsx | 6 + src/components/fields/User/Settings.tsx | 25 +++ .../fields/User/SideDrawerField.tsx | 118 +++++++++---- src/components/fields/User/UserSelect.tsx | 159 ++++++++++++++++++ src/components/fields/User/index.tsx | 9 +- 6 files changed, 377 insertions(+), 58 deletions(-) create mode 100644 src/components/fields/User/EditorCell.tsx create mode 100644 src/components/fields/User/Settings.tsx create mode 100644 src/components/fields/User/UserSelect.tsx diff --git a/src/components/fields/User/DisplayCell.tsx b/src/components/fields/User/DisplayCell.tsx index d299b6c5..a70f981e 100644 --- a/src/components/fields/User/DisplayCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -1,30 +1,108 @@ +import { useAtom } from "jotai"; +import { Avatar, AvatarGroup, ButtonBase, Stack, Tooltip } from "@mui/material"; +import { allUsersAtom, projectScope } from "@src/atoms/projectScope"; import { IDisplayCellProps } from "@src/components/fields/types"; +import { ChevronDown } from "@src/assets/icons/ChevronDown"; +import { UserDataType } from "./UserSelect"; -import { Tooltip, Stack, Avatar } from "@mui/material"; +export default function User({ + value, + showPopoverCell, + disabled, + tabIndex, +}: IDisplayCellProps) { + const [users] = useAtom(allUsersAtom, projectScope); -import { format } from "date-fns"; -import { DATE_TIME_FORMAT } from "@src/constants/dates"; + let userValue: UserDataType[] = []; + let emails = new Set(); -export default function User({ value, column }: IDisplayCellProps) { - if (!value || !value.displayName) return null; + if (value !== undefined) { + for (const user of users) { + if (user.user && user.user?.email && value.includes(user.user.email)) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + userValue.push(user.user); + } + } + } + } - const chip = ( - - - {value.displayName} + if (userValue.length === 0) { + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-end", + }} + tabIndex={tabIndex} + > + + + ); + } + + const rendered = ( + + {userValue.length > 1 ? ( + + {userValue.map((user: UserDataType) => ( + + + + ))} + + ) : ( + <> + + {userValue[0].displayName} + + )} ); - if (!value.timestamp) return chip; - - const dateLabel = format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - column.config?.format || DATE_TIME_FORMAT + if (disabled) { + return rendered; + } + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + ); - - return {chip}; } diff --git a/src/components/fields/User/EditorCell.tsx b/src/components/fields/User/EditorCell.tsx new file mode 100644 index 00000000..a65101dd --- /dev/null +++ b/src/components/fields/User/EditorCell.tsx @@ -0,0 +1,6 @@ +import { IEditorCellProps } from "@src/components/fields/types"; +import UserSelect from "./UserSelect"; + +export default function EditorCell({ ...props }: IEditorCellProps) { + return ; +} diff --git a/src/components/fields/User/Settings.tsx b/src/components/fields/User/Settings.tsx new file mode 100644 index 00000000..d1e6d3c8 --- /dev/null +++ b/src/components/fields/User/Settings.tsx @@ -0,0 +1,25 @@ +import { Typography, FormControlLabel, Checkbox } from "@mui/material"; +import { ISettingsProps } from "@src/components/fields/types"; + +export default function Settings({ onChange, config }: ISettingsProps) { + return ( + + Accept multiple value + + Make this column to support multiple values. + + + } + control={ + onChange("multiple")(e.target.checked)} + name="multiple" + /> + } + /> + ); +} diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index 1c21792a..2ec723fa 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -1,50 +1,98 @@ -import { format } from "date-fns"; +import { useRef, useState } from "react"; +import { useAtom } from "jotai"; +import { Tooltip, Stack, AvatarGroup, Avatar } from "@mui/material"; + +import { allUsersAtom, projectScope } from "@src/atoms/projectScope"; +import { fieldSx } from "@src/components/SideDrawer/utils"; +import { ChevronDown } from "@src/assets/icons/ChevronDown"; import { ISideDrawerFieldProps } from "@src/components/fields/types"; +import UserSelect, { UserDataType } from "./UserSelect"; -import { Box, Stack, Typography, Avatar } from "@mui/material"; - -import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; -import { DATE_TIME_FORMAT } from "@src/constants/dates"; - -export default function User({ +export default function SideDrawerSelect({ column, - _rowy_ref, value, - onDirty, onChange, onSubmit, disabled, }: ISideDrawerFieldProps) { - if (!value || !value.displayName || !value.timestamp) - return ; + const [open, setOpen] = useState(false); + const [users] = useAtom(allUsersAtom, projectScope); + const parentRef = useRef(null); - const dateLabel = value.timestamp - ? format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - column.config?.format || DATE_TIME_FORMAT - ) - : null; + let userValue: UserDataType[] = []; + let emails = new Set(); + + if (value !== undefined) { + for (const user of users) { + if (user.user && user.user?.email && value.includes(user.user.email)) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + userValue.push(user.user); + } + } + } + } return ( - - - - + setOpen(true)} + direction="row" + sx={[ + fieldSx, + { + alignItems: "center", + justifyContent: userValue.length > 0 ? "space-between" : "flex-end", + marginTop: "8px", + marginBottom: "8px", + }, + ]} > - {value.displayName} ({value.email}) - {dateLabel && ( - - {dateLabel} - + {userValue.length === 0 ? null : userValue.length > 1 ? ( + + {userValue.map( + (user: UserDataType) => + user && ( + + + + ) + )} + + ) : ( +
+ + {userValue[0].displayName} +
)} -
-
+ +
+ + ); } diff --git a/src/components/fields/User/UserSelect.tsx b/src/components/fields/User/UserSelect.tsx new file mode 100644 index 00000000..c2f0563d --- /dev/null +++ b/src/components/fields/User/UserSelect.tsx @@ -0,0 +1,159 @@ +import { useMemo } from "react"; +import { useAtom } from "jotai"; + +import MultiSelect from "@rowy/multiselect"; +import { + AutocompleteProps, + Avatar, + Box, + PopoverProps, + Stack, +} from "@mui/material"; +import { createFilterOptions } from "@mui/material/Autocomplete"; + +import { projectScope, allUsersAtom } from "@src/atoms/projectScope"; +import { ColumnConfig } from "@src/types/table"; + +export type UserDataType = { + email: string; + displayName?: string; + photoURL?: string; + phoneNumber?: string; +}; + +type UserOptionType = { + label: string; + value: string; + user: UserDataType; +}; + +interface IUserSelectProps { + open?: boolean; + value: T; + onChange: (value: T) => void; + onSubmit: () => void; + parentRef?: PopoverProps["anchorEl"]; + column: ColumnConfig; + disabled: boolean; + showPopoverCell: (value: boolean) => void; +} + +export default function UserSelect({ + open, + value, + onChange, + onSubmit, + parentRef, + column, + showPopoverCell, + disabled, +}: IUserSelectProps) { + const [users] = useAtom(allUsersAtom, projectScope); + + const options = useMemo(() => { + let options: UserOptionType[] = []; + let emails = new Set(); + for (const user of users) { + if (user.user && user.user?.email) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + options.push({ + label: user.user.email, + value: user.user.email, + user: user.user, + }); + } + } + } + return options; + }, [users]); + + const filterOptions = createFilterOptions({ + trim: true, + ignoreCase: true, + matchFrom: "start", + stringify: (option: UserOptionType) => option.user.displayName || "", + }); + + const renderOption: AutocompleteProps< + UserOptionType, + false, + false, + false + >["renderOption"] = (props, option) => { + return ; + }; + + return ( + { + if (typeof v === "string") { + v = [v]; + } + onChange(v); + }} + disabled={disabled} + clearText="Clear" + doneText="Done" + {...{ + AutocompleteProps: { + renderOption, + filterOptions, + }, + }} + onClose={() => { + onSubmit(); + showPopoverCell(false); + }} + // itemRenderer={(option: UserOptionType) => } + TextFieldProps={{ + style: { display: "none" }, + SelectProps: { + open: open === undefined ? true : open, + MenuProps: { + anchorEl: parentRef || null, + anchorOrigin: { vertical: "bottom", horizontal: "center" }, + transformOrigin: { vertical: "top", horizontal: "center" }, + sx: { + "& .MuiPaper-root": { minWidth: `${column.width}px !important` }, + }, + }, + }, + }} + /> + ); +} + +const UserListItem = ({ user, ...props }: { user: UserDataType }) => { + return ( +
  • + + + + {user.displayName ? user.displayName[0] : ""} + + {user.displayName} + + +
  • + ); +}; diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx index b06680df..f7d42921 100644 --- a/src/components/fields/User/index.tsx +++ b/src/components/fields/User/index.tsx @@ -4,14 +4,14 @@ import withRenderTableCell from "@src/components/Table/TableCell/withRenderTable import UserIcon from "@mui/icons-material/PersonOutlined"; import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */) ); const Settings = lazy( - () => - import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */) + () => import("./Settings" /* webpackChunkName: "Settings-User" */) ); export const config: IFieldConfig = { @@ -23,7 +23,10 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "User information and optionally, timestamp. Read-only.", - TableCell: withRenderTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, + }), SideDrawerField, settings: Settings, }; From 3941ae548ecdc79330db3c6c500645e9d2d704f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:17:26 +0000 Subject: [PATCH 04/91] Bump ua-parser-js from 0.7.32 to 0.7.33 Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.32 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.32...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d669d494..5038e845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12264,9 +12264,9 @@ typescript@^4.9.3: integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== ua-parser-js@^0.7.30: - version "0.7.32" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.32.tgz#cd8c639cdca949e30fa68c44b7813ef13e36d211" - integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== + version "0.7.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532" + integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw== unbox-primitive@^1.0.2: version "1.0.2" From 19867b64ad3fb8086cb1b235e32dbb9e9383eef2 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 28 Jan 2023 09:17:41 +0000 Subject: [PATCH 05/91] worked on backward compatibility --- src/components/fields/User/DisplayCell.tsx | 3 +++ src/components/fields/User/SideDrawerField.tsx | 3 +++ src/components/fields/User/UserSelect.tsx | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/fields/User/DisplayCell.tsx b/src/components/fields/User/DisplayCell.tsx index a70f981e..fe8d6c86 100644 --- a/src/components/fields/User/DisplayCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -17,6 +17,9 @@ export default function User({ let emails = new Set(); if (value !== undefined) { + if (!Array.isArray(value)) { + value = [value.email]; + } for (const user of users) { if (user.user && user.user?.email && value.includes(user.user.email)) { if (!emails.has(user.user.email)) { diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index 2ec723fa..82ebe309 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -23,6 +23,9 @@ export default function SideDrawerSelect({ let emails = new Set(); if (value !== undefined) { + if (!Array.isArray(value)) { + value = [value.email]; + } for (const user of users) { if (user.user && user.user?.email && value.includes(user.user.email)) { if (!emails.has(user.user.email)) { diff --git a/src/components/fields/User/UserSelect.tsx b/src/components/fields/User/UserSelect.tsx index c2f0563d..a8640a4f 100644 --- a/src/components/fields/User/UserSelect.tsx +++ b/src/components/fields/User/UserSelect.tsx @@ -84,9 +84,15 @@ export default function UserSelect({ return ; }; + if (value === undefined) { + value = []; + } else if (!Array.isArray(value)) { + value = [value.email]; + } + return ( Date: Wed, 1 Feb 2023 10:11:14 +0530 Subject: [PATCH 06/91] feat: add image reorder user interface --- src/components/Table/Styled/StyledCell.tsx | 11 ++ .../Table/TableCell/withRenderTableCell.tsx | 2 +- src/components/fields/Image/EditorCell.tsx | 153 +++++++++++------- 3 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index 5423e8e3..2d93245f 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -19,6 +19,17 @@ export const StyledCell = styled("div")(({ theme }) => ({ alignItems: "center", }, + "& > .cell-contents-contain-none": { + padding: "0 var(--cell-padding)", + width: "100%", + height: "100%", + contain: "none", + overflow: "hidden", + + display: "flex", + alignItems: "center", + }, + backgroundColor: "var(--cell-background-color)", border: `1px solid ${theme.palette.divider}`, diff --git a/src/components/Table/TableCell/withRenderTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx index 60935e07..352b2b67 100644 --- a/src/components/Table/TableCell/withRenderTableCell.tsx +++ b/src/components/Table/TableCell/withRenderTableCell.tsx @@ -191,7 +191,7 @@ export default function withRenderTableCell( if (editorMode === "inline") { return (
    diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index ceec4307..55e4cbf2 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { Fragment, useMemo } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; import { assignIn } from "lodash-es"; @@ -18,6 +18,9 @@ import useFileUpload from "@src/components/fields/File/useFileUpload"; import { IMAGE_MIME_TYPES } from "./index"; import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; + export default function Image_({ column, value, @@ -84,62 +87,100 @@ export default function Image_({ marginLeft: "0 !important", }} > - - {Array.isArray(value) && - value.map((file: FileValue, i) => ( - - { - confirm({ - title: "Delete image?", - body: "This image cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: () => handleDelete(file), - }); - }} - disabled={disabled} - tabIndex={tabIndex} - > - - - - - + console.log("Drag Ended")}> + + {(provided) => ( + + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + {(provided) => ( + +
    + +
    + { + confirm({ + title: "Delete image?", + body: "This image cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: () => handleDelete(file), + }); + }} + disabled={disabled} + tabIndex={tabIndex} + > + + + + + +
    + )} +
    + ))} + {localImages && + localImages.map((image) => ( + + + `0 0 0 1px ${theme.palette.divider} inset`, + }, + ]} + style={{ + backgroundImage: `url("${image.localURL}")`, + }} + /> + + ))} + {provided.placeholder}
    - ))} - - {localImages && - localImages.map((image) => ( - - - `0 0 0 1px ${theme.palette.divider} inset`, - }, - ]} - style={{ - backgroundImage: `url("${image.localURL}")`, - }} - /> - - ))} -
    + )} + +
    {!loading ? ( From cc54ae2f6a4b6fcaca931a67e5f4af97017e6395 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:37:20 +0000 Subject: [PATCH 07/91] worked on saving table sorts --- .../Table/ColumnHeader/ColumnHeader.tsx | 1 + .../Table/ColumnHeader/ColumnHeaderSort.tsx | 8 ++ .../Table/ColumnHeader/useSaveTableSorts.tsx | 85 +++++++++++++++++++ .../TableToolbar/Filters/Filters.tsx | 4 +- src/pages/Table/TablePage.tsx | 15 +++- src/types/table.d.ts | 1 + 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/components/Table/ColumnHeader/useSaveTableSorts.tsx diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index 40e5c5a1..9edec544 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -233,6 +233,7 @@ export const ColumnHeader = memo(function ColumnHeader({ sortKey={sortKey} currentSort={currentSort} tabIndex={focusInsideCell ? 0 : -1} + canEditColumns={canEditColumns} /> )} diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index b9d1f6ac..929b5680 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -9,6 +9,7 @@ import IconSlash, { } from "@src/components/IconSlash"; import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; +import useSaveTableSortBy from "./useSaveTableSorts"; export const SORT_STATES = ["none", "desc", "asc"] as const; @@ -16,6 +17,7 @@ export interface IColumnHeaderSortProps { sortKey: string; currentSort: typeof SORT_STATES[number]; tabIndex?: number; + canEditColumns: boolean; } /** @@ -26,15 +28,21 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({ sortKey, currentSort, tabIndex, + canEditColumns, }: IColumnHeaderSortProps) { const setTableSorts = useSetAtom(tableSortsAtom, tableScope); const nextSort = SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0]; + const trigger = useSaveTableSortBy(canEditColumns); + const handleSortClick = () => { if (nextSort === "none") setTableSorts([]); else setTableSorts([{ key: sortKey, direction: nextSort }]); + trigger([ + { key: sortKey, direction: nextSort === "none" ? "asc" : nextSort }, + ]); }; return ( diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx new file mode 100644 index 00000000..45f6d142 --- /dev/null +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -0,0 +1,85 @@ +import { useCallback, useState } from "react"; +import { useAtom } from "jotai"; +import { SnackbarKey, useSnackbar } from "notistack"; + +import LoadingButton from "@mui/lab/LoadingButton"; +import CheckIcon from "@mui/icons-material/Check"; + +import CircularProgressOptical from "@src/components/CircularProgressOptical"; +import { tableScope, updateTableSchemaAtom } from "@src/atoms/tableScope"; +import { TableSort } from "@src/types/table"; + +function useSaveTableSortBy(canEditColumns: boolean) { + const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); + if (!updateTableSchema) throw new Error("Cannot update table schema"); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const [snackbarId, setSnackbarId] = useState(null); + + // Offer to save when table sort by changes + const trigger = useCallback( + (sorts: TableSort[]) => { + if (!canEditColumns) return; + console.log(snackbarId); + if (snackbarId) { + closeSnackbar(snackbarId); + } + setSnackbarId( + enqueueSnackbar("Apply this sorting for all users?", { + action: ( + + await updateTableSchema({ sorts: sorts }) + } + /> + ), + anchorOrigin: { horizontal: "center", vertical: "top" }, + }) + ); + + return () => (snackbarId ? closeSnackbar(snackbarId) : null); + }, + [ + snackbarId, + canEditColumns, + enqueueSnackbar, + closeSnackbar, + updateTableSchema, + ] + ); + + return trigger; +} + +function SaveTableSortButton({ updateTable }: { updateTable: Function }) { + const [state, setState] = useState<"" | "loading" | "success" | "error">(""); + + const handleSaveToSchema = async () => { + setState("loading"); + try { + await updateTable(); + setState("success"); + } catch (e) { + setState("error"); + } + }; + + return ( + + ) : ( + + ) + } + > + Save + + ); +} + +export default useSaveTableSortBy; diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index b1c6b77e..8ebc32ff 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -112,7 +112,9 @@ export default function Filters() { setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index - setTableSorts([]); + if (filtersToApply.length) { + setTableSorts([]); + } }, [ hasTableFilters, hasUserFilters, diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 04fc0e61..f03f5cd8 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -1,5 +1,5 @@ -import { Suspense, lazy } from "react"; -import { useAtom } from "jotai"; +import { Suspense, lazy, useEffect, useState } from "react"; +import { useAtom, useSetAtom } from "jotai"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty, intersection } from "lodash-es"; @@ -33,6 +33,7 @@ import { tableSchemaAtom, columnModalAtom, tableModalAtom, + tableSortsAtom, } from "@src/atoms/tableScope"; import useBeforeUnload from "@src/hooks/useBeforeUnload"; import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider"; @@ -77,6 +78,7 @@ export default function TablePage({ const [tableId] = useAtom(tableIdAtom, tableScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const setTableSorts = useSetAtom(tableSortsAtom, tableScope); const snackLogContext = useSnackLogContext(); // Set permissions here so we can pass them to the `Table` component, which @@ -96,6 +98,15 @@ export default function TablePage({ useBeforeUnload(columnModalAtom, tableScope); useBeforeUnload(tableModalAtom, tableScope); + // Initially set the TableSorts values from table schema + const [setSort, setSetSort] = useState(true); + useEffect(() => { + if (setSort && Object.keys(tableSchema).length) { + setTableSorts(tableSchema.sorts || []); + setSetSort(false); + } + }, [tableSchema, setSort, setTableSorts, setSetSort]); + if (!(tableSchema as any)._rowy_ref) return ( <> diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 46a5a939..a0570542 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -101,6 +101,7 @@ export type TableSchema = { rowHeight?: number; filters?: TableFilter[]; filtersOverridable?: boolean; + sorts?: TableSort[]; functionConfigPath?: string; functionBuilderRef?: any; From f82075cb228c258a03e91a60b6814544cdfae5db Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:39:50 +0000 Subject: [PATCH 08/91] applied on column menu --- src/components/ColumnMenu/ColumnMenu.tsx | 9 +++++++++ .../Table/ColumnHeader/ColumnHeaderSort.tsx | 11 +++++++---- .../Table/ColumnHeader/useSaveTableSorts.tsx | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 8253eeb4..dec3f6fe 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -64,6 +64,7 @@ import { } from "@src/utils/table"; import { runRoutes } from "@src/constants/runRoutes"; import { useSnackLogContext } from "@src/contexts/SnackLogContext"; +import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts"; export interface IMenuModalProps { name: string; @@ -116,6 +117,8 @@ export default function ColumnMenu({ const [altPress] = useAtom(altPressAtom, projectScope); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); + if (!columnMenu) return null; const { column, anchorEl } = columnMenu; if (column.type === FieldType.last) return null; @@ -189,6 +192,9 @@ export default function ColumnMenu({ setTableSorts( isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }] ); + if (!isSorted || isAsc) { + triggerSaveTableSorts([{ key: sortKey, direction: "desc" }]); + } handleClose(); }, active: isSorted && !isAsc, @@ -203,6 +209,9 @@ export default function ColumnMenu({ setTableSorts( isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }] ); + if (!isSorted || !isAsc) { + triggerSaveTableSorts([{ key: sortKey, direction: "asc" }]); + } handleClose(); }, active: isSorted && isAsc, diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index 929b5680..45e924f0 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -9,7 +9,7 @@ import IconSlash, { } from "@src/components/IconSlash"; import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; -import useSaveTableSortBy from "./useSaveTableSorts"; +import useSaveTableSorts from "./useSaveTableSorts"; export const SORT_STATES = ["none", "desc", "asc"] as const; @@ -35,13 +35,16 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({ const nextSort = SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0]; - const trigger = useSaveTableSortBy(canEditColumns); + const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); const handleSortClick = () => { if (nextSort === "none") setTableSorts([]); else setTableSorts([{ key: sortKey, direction: nextSort }]); - trigger([ - { key: sortKey, direction: nextSort === "none" ? "asc" : nextSort }, + triggerSaveTableSorts([ + { + key: sortKey, + direction: nextSort === "none" ? "asc" : nextSort, + }, ]); }; diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx index 45f6d142..fb304674 100644 --- a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -9,13 +9,13 @@ import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { tableScope, updateTableSchemaAtom } from "@src/atoms/tableScope"; import { TableSort } from "@src/types/table"; -function useSaveTableSortBy(canEditColumns: boolean) { +function useSaveTableSorts(canEditColumns: boolean) { const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); if (!updateTableSchema) throw new Error("Cannot update table schema"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [snackbarId, setSnackbarId] = useState(null); - // Offer to save when table sort by changes + // Offer to save when table sorts changes const trigger = useCallback( (sorts: TableSort[]) => { if (!canEditColumns) return; @@ -82,4 +82,4 @@ function SaveTableSortButton({ updateTable }: { updateTable: Function }) { ); } -export default useSaveTableSortBy; +export default useSaveTableSorts; From cd65a5cbcfedb370fb1ea7789cf907dd2761d14a Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 4 Feb 2023 21:50:48 +0530 Subject: [PATCH 09/91] variable name changed --- src/pages/Table/TablePage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index f03f5cd8..15b5ed59 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -99,13 +99,13 @@ export default function TablePage({ useBeforeUnload(tableModalAtom, tableScope); // Initially set the TableSorts values from table schema - const [setSort, setSetSort] = useState(true); + const [applySort, setApplySort] = useState(true); useEffect(() => { - if (setSort && Object.keys(tableSchema).length) { + if (applySort && Object.keys(tableSchema).length) { setTableSorts(tableSchema.sorts || []); - setSetSort(false); + setApplySort(false); } - }, [tableSchema, setSort, setTableSorts, setSetSort]); + }, [tableSchema, applySort, setTableSorts, setApplySort]); if (!(tableSchema as any)._rowy_ref) return ( From d655c2699c0c8ed67a18d36f3c6a2b05cbf9a15d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Feb 2023 21:13:52 +0530 Subject: [PATCH 10/91] feat: add image reorder functionality --- src/components/fields/File/useFileUpload.ts | 10 +++++ src/components/fields/Image/EditorCell.tsx | 50 +++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/components/fields/File/useFileUpload.ts b/src/components/fields/File/useFileUpload.ts index d99ccf67..22795349 100644 --- a/src/components/fields/File/useFileUpload.ts +++ b/src/components/fields/File/useFileUpload.ts @@ -75,6 +75,15 @@ export default function useFileUpload( [deleteUpload, docRef, fieldName, updateField] ); + // Drag and Drop + const handleUpdate = (files: any) => { + updateField({ + path: docRef.path, + fieldName, + value: files, + }); + }; + return { localFiles, progress, @@ -83,5 +92,6 @@ export default function useFileUpload( handleUpload, handleDelete, dropzoneState, + handleUpdate, }; } diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index 55e4cbf2..181b19e9 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo } from "react"; +import { useMemo } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; import { assignIn } from "lodash-es"; @@ -19,7 +19,13 @@ import { IMAGE_MIME_TYPES } from "./index"; import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; export default function Image_({ column, @@ -31,11 +37,17 @@ export default function Image_({ }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { - multiple: true, - accept: IMAGE_MIME_TYPES, - }); + const { + loading, + progress, + handleDelete, + localFiles, + dropzoneState, + handleUpdate, + } = useFileUpload(_rowy_ref, column.key, { + multiple: true, + accept: IMAGE_MIME_TYPES, + }); const localImages = useMemo( () => @@ -48,6 +60,28 @@ export default function Image_({ const { getRootProps, getInputProps, isDragActive } = dropzoneState; const dropzoneProps = getRootProps(); + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + const { destination, source, draggableId } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + const newValue = Array.from(value); + + newValue.splice(source.index, 1); + newValue.splice(destination.index, 0, value[source.index]); + + handleUpdate([...newValue]); + }; + let thumbnailSize = "100x100"; if (rowHeight > 50) thumbnailSize = "200x200"; if (rowHeight > 100) thumbnailSize = "400x400"; @@ -87,7 +121,7 @@ export default function Image_({ marginLeft: "0 !important", }} > - console.log("Drag Ended")}> + {(provided) => ( Date: Tue, 7 Feb 2023 10:53:23 +0530 Subject: [PATCH 11/91] removed info icon --- .../Breadcrumbs/BreadcrumbsTableRoot.tsx | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx index 676408e3..9eea60b2 100644 --- a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx +++ b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx @@ -1,6 +1,6 @@ import { useAtom } from "jotai"; import { useParams, Link as RouterLink } from "react-router-dom"; -import { find, camelCase, uniq } from "lodash-es"; +import { find, camelCase } from "lodash-es"; import { Stack, @@ -12,9 +12,6 @@ import { } from "@mui/material"; import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined"; -import InfoTooltip from "@src/components/InfoTooltip"; -import RenderedMarkdown from "@src/components/RenderedMarkdown"; - import { projectScope, userRolesAtom, @@ -83,28 +80,6 @@ export default function BreadcrumbsTableRoot(props: StackProps) { )} - - {tableSettings.description && ( - - - - } - buttonLabel="Table info" - tooltipProps={{ - componentsProps: { - popper: { sx: { zIndex: "appBar" } }, - tooltip: { sx: { maxWidth: "75vw" } }, - } as any, - }} - defaultOpen={!dismissed.includes(tableSettings.id)} - onClose={() => setDismissed((d) => uniq([...d, tableSettings.id]))} - /> - )} ); } From 2c77eeb8f86ca89d3f19c4653432f947c64ee2e9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Feb 2023 21:47:25 +0530 Subject: [PATCH 12/91] feat: add drag and drop to file field --- src/components/fields/File/EditorCell.tsx | 206 +++++++++++++++------- 1 file changed, 143 insertions(+), 63 deletions(-) diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index 247c99fa..722491e7 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -15,6 +15,15 @@ import { DATE_TIME_FORMAT } from "@src/constants/dates"; import { FileValue } from "@src/types/table"; import useFileUpload from "./useFileUpload"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; + export default function File_({ column, value, @@ -25,11 +34,40 @@ export default function File_({ }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { multiple: true }); + const { + loading, + progress, + handleDelete, + localFiles, + dropzoneState, + handleUpdate, + } = useFileUpload(_rowy_ref, column.key, { multiple: true }); const { isDragActive, getRootProps, getInputProps } = dropzoneState; const dropzoneProps = getRootProps(); + + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + const { destination, source, draggableId } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + const newValue = Array.from(value); + + newValue.splice(source.index, 1); + newValue.splice(destination.index, 0, value[source.index]); + + handleUpdate([...newValue]); + }; + return ( - - {Array.isArray(value) && - value.map((file: FileValue) => ( - 1 ? { maxWidth: `calc(100% - 12px)` } : {} - } - > - + + {(provided) => ( + + - } - sx={{ - "& .MuiChip-label": { - lineHeight: 5 / 3, - }, - }} - onClick={(e: any) => e.stopPropagation()} - component="a" - href={file.downloadURL} - target="_blank" - rel="noopener noreferrer" - clickable - onDelete={ - disabled - ? undefined - : () => - confirm({ - handleConfirm: () => handleDelete(file), - title: "Delete file?", - body: "This file cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - }) - } - tabIndex={tabIndex} - style={{ width: "100%", cursor: "pointer" }} - /> - - - ))} - {localFiles && - localFiles.map((file) => ( - - } - label={file.name} - deleteIcon={ - - } - /> - - ))} - + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + {(provided) => ( + 1 + ? { + maxWidth: `calc(100% - 12px)`, + display: "flex", + alignItems: "center", + ...provided.draggableProps.style, + } + : {} + } + > +
    + +
    + + } + sx={{ + "& .MuiChip-label": { + lineHeight: 5 / 3, + }, + }} + onClick={(e: any) => e.stopPropagation()} + component="a" + href={file.downloadURL} + target="_blank" + rel="noopener noreferrer" + clickable + onDelete={ + disabled + ? undefined + : () => + confirm({ + handleConfirm: () => handleDelete(file), + title: "Delete file?", + body: "This file cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + }) + } + tabIndex={tabIndex} + style={{ width: "100%", cursor: "pointer" }} + /> + +
    + )} +
    + ))} +
    + + {localFiles && + localFiles.map((file) => ( + + } + label={file.name} + deleteIcon={ + + } + /> + + ))} + + )} +
    +
    {!loading ? ( !disabled && ( From d621afca6d784d7c8062df4bc188e06241b52cbe Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 11 Feb 2023 19:02:11 +0530 Subject: [PATCH 13/91] Added new filed ARRAY --- src/components/fields/Array/DisplayCell.tsx | 24 ++ .../Array/SideDrawerField/AddButton.tsx | 92 ++++++++ .../Array/SideDrawerField/SupportedTypes.ts | 105 +++++++++ .../fields/Array/SideDrawerField/index.tsx | 205 ++++++++++++++++++ .../fields/Array/SideDrawerField/utils.ts | 59 +++++ src/components/fields/Array/index.tsx | 30 +++ src/components/fields/index.ts | 2 + src/constants/fields.ts | 1 + 8 files changed, 518 insertions(+) create mode 100644 src/components/fields/Array/DisplayCell.tsx create mode 100644 src/components/fields/Array/SideDrawerField/AddButton.tsx create mode 100644 src/components/fields/Array/SideDrawerField/SupportedTypes.ts create mode 100644 src/components/fields/Array/SideDrawerField/index.tsx create mode 100644 src/components/fields/Array/SideDrawerField/utils.ts create mode 100644 src/components/fields/Array/index.tsx diff --git a/src/components/fields/Array/DisplayCell.tsx b/src/components/fields/Array/DisplayCell.tsx new file mode 100644 index 00000000..e934ea5f --- /dev/null +++ b/src/components/fields/Array/DisplayCell.tsx @@ -0,0 +1,24 @@ +import { useTheme } from "@mui/material"; +import { IDisplayCellProps } from "@src/components/fields/types"; + +export default function Array({ value }: IDisplayCellProps) { + const theme = useTheme(); + + if (!value) { + return null; + } + + return ( +
    + {JSON.stringify(value, null, 4)} +
    + ); +} diff --git a/src/components/fields/Array/SideDrawerField/AddButton.tsx b/src/components/fields/Array/SideDrawerField/AddButton.tsx new file mode 100644 index 00000000..4719970e --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/AddButton.tsx @@ -0,0 +1,92 @@ +import { useRef, useState } from "react"; +import { + Button, + ButtonGroup, + ListItemText, + MenuItem, + Select, +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; + +import { ChevronDown as ArrowDropDownIcon } from "@src/assets/icons"; +import { FieldType } from "@src/components/fields/types"; +import { getFieldProp } from "@src/components/fields"; + +import { + ArraySupportedFields, + ArraySupportedFiledTypes, +} from "./SupportedTypes"; + +function AddButton({ handleAddNew }: { handleAddNew: Function }) { + const anchorEl = useRef(null); + const [open, setOpen] = useState(false); + const [fieldType, setFieldType] = useState( + FieldType.shortText + ); + + return ( + <> + + + + + + + + + ); +} + +export default AddButton; diff --git a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts new file mode 100644 index 00000000..28acd30c --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts @@ -0,0 +1,105 @@ +import { DocumentReference, GeoPoint, Timestamp } from "firebase/firestore"; + +import { FieldType } from "@src/components/fields/types"; + +import NumberValueSidebar from "@src/components/fields/Number/SideDrawerField"; +import ShortTextValueSidebar from "@src/components/fields/ShortText/SideDrawerField"; +import JsonValueSidebar from "@src/components/fields/Json/SideDrawerField"; +import CheckBoxValueSidebar from "@src/components/fields/Checkbox/SideDrawerField"; +import GeoPointValueSidebar from "@src/components/fields/GeoPoint/SideDrawerField"; +import DateTimeValueSidebar from "@src/components/fields/DateTime/SideDrawerField"; +import ReferenceValueSidebar from "@src/components/fields/Reference/SideDrawerField"; + +export const ArraySupportedFields = [ + FieldType.number, + FieldType.shortText, + FieldType.json, + FieldType.checkbox, + FieldType.geoPoint, + FieldType.dateTime, + FieldType.reference, +] as const; + +export type ArraySupportedFiledTypes = typeof ArraySupportedFields[number]; + +export const SupportedTypes = { + [FieldType.number]: { + Sidebar: NumberValueSidebar, + initialValue: 0, + dataType: "common", + instance: Object, + }, + [FieldType.shortText]: { + Sidebar: ShortTextValueSidebar, + initialValue: "", + dataType: "common", + instance: Object, + }, + [FieldType.checkbox]: { + Sidebar: CheckBoxValueSidebar, + initialValue: false, + dataType: "common", + instance: Object, + }, + [FieldType.json]: { + Sidebar: JsonValueSidebar, + initialValue: {}, + sx: [ + { + marginTop: "24px", + }, + ], + dataType: "common", + instance: Object, + }, + [FieldType.geoPoint]: { + Sidebar: GeoPointValueSidebar, + initialValue: new GeoPoint(0, 0), + dataType: "firestore-type", + instance: GeoPoint, + }, + [FieldType.dateTime]: { + Sidebar: DateTimeValueSidebar, + initialValue: Timestamp.now(), + dataType: "firestore-type", + instance: Timestamp, + }, + [FieldType.reference]: { + Sidebar: ReferenceValueSidebar, + initialValue: null, + dataType: "firestore-type", + instance: DocumentReference, + }, +}; + +export function detectType(value: any): ArraySupportedFiledTypes { + if (value === null) { + return FieldType.reference; + } + for (const supportedField of ArraySupportedFields) { + if (SupportedTypes[supportedField].dataType === "firestore-type") { + if (value instanceof SupportedTypes[supportedField].instance) { + return supportedField; + } + } + } + + switch (typeof value) { + case "bigint": + case "number": { + return FieldType.number; + } + case "string": { + return FieldType.shortText; + } + case "boolean": { + return FieldType.checkbox; + } + case "object": { + return FieldType.json; + } + default: { + return FieldType.shortText; + } + } +} diff --git a/src/components/fields/Array/SideDrawerField/index.tsx b/src/components/fields/Array/SideDrawerField/index.tsx new file mode 100644 index 00000000..b8415eb5 --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/index.tsx @@ -0,0 +1,205 @@ +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; + +import { Stack, Box, Button, ListItem, List } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; +import DeleteIcon from "@mui/icons-material/DeleteOutline"; + +import { FieldType, ISideDrawerFieldProps } from "@src/components/fields/types"; +import { TableRowRef } from "@src/types/table"; + +import AddButton from "./AddButton"; +import { getPseudoColumn } from "./utils"; +import { + ArraySupportedFiledTypes, + detectType, + SupportedTypes, +} from "./SupportedTypes"; + +function ArrayFieldInput({ + onChange, + value, + _rowy_ref, + index, + onRemove, + onSubmit, + id, +}: { + index: number; + onRemove: (index: number) => void; + onChange: (value: any) => void; + value: any; + onSubmit: () => void; + _rowy_ref: TableRowRef; + id: string; +}) { + const typeDetected = detectType(value); + + const Sidebar = SupportedTypes[typeDetected].Sidebar; + return ( + + {(provided) => ( + + + + false ? theme.palette.action.disabledOpacity : 1, + }, + ]} + /> + + + + + onRemove(index)} + > + + + + )} + + ); +} + +export default function ArraySideDrawerField({ + column, + value, + onChange, + onSubmit, + disabled, + _rowy_ref, + onDirty, + ...props +}: ISideDrawerFieldProps) { + const handleAddNew = (fieldType: ArraySupportedFiledTypes) => { + onChange([...(value || []), SupportedTypes[fieldType].initialValue]); + onDirty(true); + }; + const handleChange = (newValue_: any, indexUpdated: number) => { + onChange( + [...(value || [])].map((v: any, i) => { + if (i === indexUpdated) { + return newValue_; + } + + return v; + }) + ); + }; + + const handleRemove = (index: number) => { + value.splice(index, 1); + onChange([...value]); + onDirty(true); + onSubmit(); + }; + + const handleClearField = () => { + onChange([]); + onSubmit(); + }; + + function handleOnDragEnd(result: DropResult) { + if ( + !result.destination || + result.destination.index === result.source.index + ) { + return; + } + const list = Array.from(value); + const [removed] = list.splice(result.source.index, 1); + list.splice(result.destination.index, 0, removed); + onChange(list); + onSubmit(); + } + + if (value === undefined || Array.isArray(value)) { + return ( + <> + + + {(provided) => ( + + {(value || []).map((v: any, index: number) => ( + handleChange(newValue, index)} + onRemove={handleRemove} + index={index} + onSubmit={onSubmit} + /> + ))} + {provided.placeholder} + + )} + + + + + ); + } + + return ( + + + {JSON.stringify(value, null, 4)} + + + + ); +} diff --git a/src/components/fields/Array/SideDrawerField/utils.ts b/src/components/fields/Array/SideDrawerField/utils.ts new file mode 100644 index 00000000..599c4694 --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/utils.ts @@ -0,0 +1,59 @@ +import { ColumnConfig } from "@src/types/table"; +import { FieldType } from "@src/constants/fields"; +import { ArraySupportedFiledTypes } from "./SupportedTypes"; +import { GeoPoint, DocumentReference } from "firebase/firestore"; +export function getPseudoColumn( + fieldType: FieldType, + index: number, + value: any +): ColumnConfig { + return { + fieldName: (+new Date()).toString(), + index: index, + key: (+new Date()).toString(), + name: value + "", + type: fieldType, + }; +} + +// archive: detectType / TODO: remove +export function detectType(value: any): ArraySupportedFiledTypes { + if (value === null) { + return FieldType.reference; + } + console.log(typeof GeoPoint); + console.log(value instanceof DocumentReference, value); + + if (typeof value === "object") { + const keys = Object.keys(value); + // console.log({ keys, value }, typeof value); + if (keys.length === 2) { + if (keys.includes("_lat") && keys.includes("_long")) { + return FieldType.geoPoint; + } + if (keys.includes("nanoseconds") && keys.includes("seconds")) { + return FieldType.dateTime; + } + } + if (+new Date(value)) { + return FieldType.dateTime; + } + return FieldType.json; + } + + switch (typeof value) { + case "bigint": + case "number": { + return FieldType.number; + } + case "string": { + return FieldType.shortText; + } + case "boolean": { + return FieldType.checkbox; + } + default: { + return FieldType.shortText; + } + } +} diff --git a/src/components/fields/Array/index.tsx b/src/components/fields/Array/index.tsx new file mode 100644 index 00000000..9aad247c --- /dev/null +++ b/src/components/fields/Array/index.tsx @@ -0,0 +1,30 @@ +import { lazy } from "react"; +import DataArrayIcon from "@mui/icons-material/DataArray"; + +import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; + +import DisplayCell from "./DisplayCell"; + +const SideDrawerField = lazy( + () => + import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Array" */) +); + +export const config: IFieldConfig = { + type: FieldType.array, + name: "Array", + group: "Code", + dataType: "object", + initialValue: [], + initializable: true, + icon: , + description: + "Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.", + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1, minWidth: "200px" } } }, + }), + SideDrawerField, + requireConfiguration: true, +}; +export default config; diff --git a/src/components/fields/index.ts b/src/components/fields/index.ts index 0d54b0a5..4b16d1f2 100644 --- a/src/components/fields/index.ts +++ b/src/components/fields/index.ts @@ -31,6 +31,7 @@ import ConnectTable from "./ConnectTable"; import ConnectService from "./ConnectService"; import Json from "./Json"; import Code from "./Code"; +import Array from "./Array"; import Action from "./Action"; import Derivative from "./Derivative"; import Formula from "./Formula"; @@ -82,6 +83,7 @@ export const FIELDS: IFieldConfig[] = [ Json, Code, Markdown, + Array, /** CLOUD FUNCTION */ Action, Derivative, diff --git a/src/constants/fields.ts b/src/constants/fields.ts index 900b88db..b05c6b89 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -35,6 +35,7 @@ export enum FieldType { json = "JSON", code = "CODE", markdown = "MARKDOWN", + array = "ARRAY", // CLOUD FUNCTION action = "ACTION", derivative = "DERIVATIVE", From 1464b633382b1230a77c7643909b8de6d056d4d3 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 11 Feb 2023 19:09:03 +0530 Subject: [PATCH 14/91] date-time detect fix --- src/components/fields/Array/SideDrawerField/SupportedTypes.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts index 28acd30c..6f3fc20b 100644 --- a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts +++ b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts @@ -96,6 +96,9 @@ export function detectType(value: any): ArraySupportedFiledTypes { return FieldType.checkbox; } case "object": { + if (+new Date(value)) { + return FieldType.dateTime; + } return FieldType.json; } default: { From 07957585cab566dcea1fe1e10aff80d1bc1c2011 Mon Sep 17 00:00:00 2001 From: Harini Janakiraman Date: Mon, 13 Feb 2023 12:44:40 +1100 Subject: [PATCH 15/91] Update README.md --- README.md | 74 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4e22728f..e4aa71b8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,14 @@ -

    -Rowy -

    + + + -

    -✨ Low-code backend ✨
    -

    -Manage your database and build automations as easy as using a spreadsheet. +✨ Airtable-like UI for managing database ✨ Build any automation, with or without code ✨

    -Connect to your database (Firestore), manage data on an Airtable-like UI with role based access controls. Build cloud function workflows in JS/TS using any NPM or APIs. Forget CLIs, configs, and DevOps. Focus on building your apps with a platform designed for developer productivity. Low-code for Firebase and Google Cloud. +Connect to your database and create Cloud Functions in low-code - without leaving your browser.
    +Focus on building your apps. +Low-code for Firebase and Google Cloud.

    @@ -27,27 +26,35 @@ Connect to your database (Firestore), manage data on an Airtable-like UI with ro [![GitHub stars](https://img.shields.io/github/stars/rowyio/rowy)](https://github.com/rowyio/rowy/stargazers/)
    - ## Live Demo 💥 Check out the [live demo](https://demo.rowy.io/) of Rowy 💥 -## Quick Deploy - -Set up Rowy on your Google Cloud Platform project with this easy deploy button. - -[Guided quick start button](https://rowy.app/) - -https://rowy.app - -## Documentation - -You can find the full documentation with how-to guides and templates -[here](http://docs.rowy.io/). - ## Features + https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-9589-d7defbf7a63f.mp4