diff --git a/package.json b/package.json index 623d898d..86504852 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-router-dom": "^5.0.1", "react-scripts": "^4.0.3", "react-scroll-sync": "^0.8.0", + "react-use-search": "^0.3.1", "react-usestateref": "^1.0.5", "serve": "^11.3.2", "tinymce": "^5.2.0", diff --git a/src/assets/icons/Copy.tsx b/src/assets/icons/Copy.tsx new file mode 100644 index 00000000..6c3e5125 --- /dev/null +++ b/src/assets/icons/Copy.tsx @@ -0,0 +1,9 @@ +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function Copy(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/components/FloatingSearch.tsx b/src/components/FloatingSearch.tsx new file mode 100644 index 00000000..ed4815d6 --- /dev/null +++ b/src/components/FloatingSearch.tsx @@ -0,0 +1,66 @@ +import { + useScrollTrigger, + Paper, + TextField, + FilledTextFieldProps, + InputAdornment, +} from "@material-ui/core"; +import SearchIcon from "@material-ui/icons/Search"; + +export interface IFloatingSearchProps extends Partial { + label: string; +} + +export default function FloatingSearch({ + label, + ...props +}: IFloatingSearchProps) { + const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 }); + + return ( + theme.spacing(7 + 1), + zIndex: "appBar", + }} + > + + + + ), + }} + sx={{ + "& .MuiInputLabel-root": { + opacity: 0, + mt: -5, + mb: 2, + }, + "& .MuiFilledInput-root": { + boxShadow: 0, + borderRadius: 2, + "&::before": { + borderRadius: 2, + height: (theme) => (theme.shape.borderRadius as number) * 4, + }, + }, + }} + {...props} + /> + + ); +} diff --git a/src/components/Settings/UserManagement/UserItem.tsx b/src/components/Settings/UserManagement/UserItem.tsx new file mode 100644 index 00000000..b3be0631 --- /dev/null +++ b/src/components/Settings/UserManagement/UserItem.tsx @@ -0,0 +1,104 @@ +import { + ListItem, + ListItemAvatar, + Avatar, + ListItemText, + Tooltip, + IconButton, +} from "@material-ui/core"; +import CopyIcon from "assets/icons/Copy"; +import DeleteIcon from "@material-ui/icons/DeleteOutlined"; + +import MultiSelect from "@antlerengineering/multiselect"; +import { User } from "pages/Settings/UserManagement"; + +export default function UserItem({ + id, + user: { displayName, email, photoURL }, +}: User) { + return ( + + + SM + + + + } + secondaryAction={ + <> + + + + navigator.clipboard.writeText(id)} + > + + + + + + + + + + } + sx={{ + pr: 1, + + "& .MuiListItemSecondaryAction-root": { + position: "static", + transform: "none", + marginLeft: "auto", + + display: "flex", + alignItems: "center", + }, + }} + /> + ); +} diff --git a/src/components/Settings/UserManagement/UserSkeleton.tsx b/src/components/Settings/UserManagement/UserSkeleton.tsx new file mode 100644 index 00000000..80dcc9e7 --- /dev/null +++ b/src/components/Settings/UserManagement/UserSkeleton.tsx @@ -0,0 +1,40 @@ +import { + Skeleton, + ListItem, + ListItemAvatar, + Avatar, + ListItemText, + Stack, +} from "@material-ui/core"; + +export default function UserSkeleton() { + return ( + + + + + + + } + secondary={} + /> + + } + secondaryAction={ + + + + + + } + /> + ); +} diff --git a/src/components/Table/formatters/FinalColumn.tsx b/src/components/Table/formatters/FinalColumn.tsx index fc2859a7..dfc3fd60 100644 --- a/src/components/Table/formatters/FinalColumn.tsx +++ b/src/components/Table/formatters/FinalColumn.tsx @@ -4,9 +4,9 @@ import { FormatterProps } from "react-data-grid"; import { makeStyles, createStyles } from "@material-ui/styles"; import { Grid, Tooltip, IconButton } from "@material-ui/core"; import CopyCellsIcon from "assets/icons/CopyCells"; -import DeleteIcon from "@material-ui/icons/DeleteForever"; +import DeleteIcon from "@material-ui/icons/DeleteOutlined"; -import { SnackContext } from "contexts/SnackContext"; +// import { SnackContext } from "contexts/SnackContext"; import { useConfirmation } from "components/ConfirmationDialog/Context"; import { useRowyContext } from "contexts/RowyContext"; import useKeyPress from "hooks/useKeyPress"; @@ -33,13 +33,13 @@ export default function FinalColumn({ row }: FormatterProps) { const { requestConfirmation } = useConfirmation(); const { tableActions } = useRowyContext(); const shiftPress = useKeyPress("Shift"); - const snack = useContext(SnackContext); + // const snack = useContext(SnackContext); const handleDelete = async () => tableActions?.row.delete(row.id); return ( - + ) { }); if (tableActions) tableActions?.row.add(clonedRow); }} - aria-label="Duplicate row" + aria-label="Duplicate Row" > @@ -66,13 +66,13 @@ export default function FinalColumn({ row }: FormatterProps) { - + {shiftPress ? ( diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx index d6f45ba9..b76d35bd 100644 --- a/src/pages/Settings/ProjectSettings.tsx +++ b/src/pages/Settings/ProjectSettings.tsx @@ -21,7 +21,7 @@ export interface IProjectSettingsChildProps { updatePublicSettings: (data: Record) => void; } -export default function ProjectSettings() { +export default function ProjectSettingsPage() { const snack = useSnackContext(); const [settingsState] = useDoc({ path: SETTINGS }, { createIfMissing: true }); diff --git a/src/pages/Settings/UserManagement.tsx b/src/pages/Settings/UserManagement.tsx index ab2099a0..6f3805c5 100644 --- a/src/pages/Settings/UserManagement.tsx +++ b/src/pages/Settings/UserManagement.tsx @@ -1,13 +1,72 @@ -import { Container, Stack } from "@material-ui/core"; +import { useSearch } from "react-use-search"; -import SettingsSection from "components/Settings/SettingsSection"; +import { Container, Stack, Typography, Paper, List } from "@material-ui/core"; + +import FloatingSearch from "components/FloatingSearch"; +import UserItem from "components/Settings/UserManagement/UserItem"; +import UserSkeleton from "components/Settings/UserManagement/UserSkeleton"; + +import useCollection from "hooks/useCollection"; +import { USERS } from "config/dbPaths"; + +export interface User { + id: string; + user: { + displayName: string; + email: string; + photoURL: string; + }; +} + +export default function UserManagementPage() { + const [usersState] = useCollection({ path: USERS }); + const loading = usersState.loading || !Array.isArray(usersState.documents); + + const [filteredUsers, query, handleChange] = useSearch( + usersState.documents ?? [], + (user, query) => + user.id === query || + user.user.displayName.includes(query) || + user.user.email.includes(query), + { filter: true, debounce: 200 } + ); -export default function UserManagement() { return ( - - TODO: + + + + + Users + + {!loading && ( + + {query + ? `${filteredUsers.length} of ${usersState.documents.length}` + : usersState.documents.length} + + )} + + + + {loading || (query === "" && filteredUsers.length === 0) ? ( + <> + + + + + ) : ( + filteredUsers.map((user) => ) + )} + + ); } diff --git a/src/pages/Settings/UserSettings.tsx b/src/pages/Settings/UserSettings.tsx index ca359bb2..eb88fb0a 100644 --- a/src/pages/Settings/UserSettings.tsx +++ b/src/pages/Settings/UserSettings.tsx @@ -2,7 +2,7 @@ import { Container, Stack } from "@material-ui/core"; import SettingsSection from "components/Settings/SettingsSection"; -export default function UserSettings() { +export default function UserSettingsPage() { return ( diff --git a/yarn.lock b/yarn.lock index 85f70e35..b1a26128 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13740,6 +13740,13 @@ react-transition-group@^4.4.0, react-transition-group@^4.4.1: loose-envify "^1.4.0" prop-types "^15.6.2" +react-use-search@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/react-use-search/-/react-use-search-0.3.1.tgz#1a36434a0f0a0fc2600a743a2c170ad88f718787" + integrity sha512-Ec8+5AnOMunnA532ziNLzs3W/4l9MhecvRF1fi2ho9JvXBgT/pmdFpgN3fRfPCHAsMf9LpEo4LxDZDs/BdTfNQ== + dependencies: + lodash.debounce "^4.0.8" + react-usestateref@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/react-usestateref/-/react-usestateref-1.0.5.tgz#0b5659217022a7e88087875ec1745730940b7882"