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"