mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
add user management UI
This commit is contained in:
@@ -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",
|
||||
|
||||
9
src/assets/icons/Copy.tsx
Normal file
9
src/assets/icons/Copy.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
|
||||
export default function Copy(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d="M18 7a2 2 0 012 2v10a2 2 0 01-2 2h-8a2 2 0 01-2-2V9a2 2 0 012-2h8zm0 2h-8v10h8V9zM4 15V5a2 2 0 012-2h8a2 2 0 012 2H6v12a2 2 0 01-2-2z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
66
src/components/FloatingSearch.tsx
Normal file
66
src/components/FloatingSearch.tsx
Normal file
@@ -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<FilledTextFieldProps> {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export default function FloatingSearch({
|
||||
label,
|
||||
...props
|
||||
}: IFloatingSearchProps) {
|
||||
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={trigger ? 8 : 1}
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: (theme) => theme.spacing(7 + 1),
|
||||
zIndex: "appBar",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={label}
|
||||
placeholder={label}
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
type="search"
|
||||
id="user-management-search"
|
||||
size="medium"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment
|
||||
position="start"
|
||||
sx={{ px: 0.5, pointerEvents: "none" }}
|
||||
>
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
104
src/components/Settings/UserManagement/UserItem.tsx
Normal file
104
src/components/Settings/UserManagement/UserItem.tsx
Normal file
@@ -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 (
|
||||
<ListItem
|
||||
children={
|
||||
<>
|
||||
<ListItemAvatar>
|
||||
<Avatar src={photoURL}>SM</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={displayName}
|
||||
secondary={email}
|
||||
primaryTypographyProps={{
|
||||
style: { userSelect: "all" },
|
||||
}}
|
||||
secondaryTypographyProps={{
|
||||
noWrap: true,
|
||||
style: { userSelect: "all" },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
secondaryAction={
|
||||
<>
|
||||
<MultiSelect
|
||||
label="Roles"
|
||||
value={["ADMIN"]}
|
||||
options={["ADMIN"]}
|
||||
onChange={console.log}
|
||||
TextFieldProps={{
|
||||
fullWidth: false,
|
||||
|
||||
sx: {
|
||||
mr: 0.5,
|
||||
|
||||
"& .MuiInputLabel-root": {
|
||||
opacity: 0,
|
||||
mt: -3,
|
||||
},
|
||||
|
||||
"& .MuiFilledInput-root": {
|
||||
boxShadow: 0,
|
||||
"&::before": { content: "none" },
|
||||
|
||||
"&:hover, &.Mui-focused": { bgcolor: "action.hover" },
|
||||
},
|
||||
"& .MuiSelect-select.MuiFilledInput-input": {
|
||||
typography: "button",
|
||||
pl: 1,
|
||||
pr: 3.5,
|
||||
},
|
||||
"& .MuiSelect-icon": {
|
||||
right: 2,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tooltip title="Copy UID">
|
||||
<IconButton
|
||||
aria-label="Copy UID"
|
||||
onClick={() => navigator.clipboard.writeText(id)}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete">
|
||||
<IconButton aria-label="Delete" color="error">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
pr: 1,
|
||||
|
||||
"& .MuiListItemSecondaryAction-root": {
|
||||
position: "static",
|
||||
transform: "none",
|
||||
marginLeft: "auto",
|
||||
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
40
src/components/Settings/UserManagement/UserSkeleton.tsx
Normal file
40
src/components/Settings/UserManagement/UserSkeleton.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
Skeleton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
Avatar,
|
||||
ListItemText,
|
||||
Stack,
|
||||
} from "@material-ui/core";
|
||||
|
||||
export default function UserSkeleton() {
|
||||
return (
|
||||
<ListItem
|
||||
children={
|
||||
<>
|
||||
<ListItemAvatar>
|
||||
<Skeleton variant="circular">
|
||||
<Avatar />
|
||||
</Skeleton>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<Skeleton width={80} />}
|
||||
secondary={<Skeleton width={120} />}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
secondaryAction={
|
||||
<Stack
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
sx={{ pr: 1.25 }}
|
||||
>
|
||||
<Skeleton width={80} height={32} />
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<any, any>) {
|
||||
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 (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Tooltip title="Duplicate row">
|
||||
<Tooltip title="Duplicate Row">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
@@ -58,7 +58,7 @@ export default function FinalColumn({ row }: FormatterProps<any, any>) {
|
||||
});
|
||||
if (tableActions) tableActions?.row.add(clonedRow);
|
||||
}}
|
||||
aria-label="Duplicate row"
|
||||
aria-label="Duplicate Row"
|
||||
>
|
||||
<CopyCellsIcon />
|
||||
</IconButton>
|
||||
@@ -66,13 +66,13 @@ export default function FinalColumn({ row }: FormatterProps<any, any>) {
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Tooltip title="Delete row">
|
||||
<Tooltip title="Delete Row">
|
||||
{shiftPress ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
onClick={handleDelete}
|
||||
aria-label="Delete row"
|
||||
aria-label="Delete Row"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface IProjectSettingsChildProps {
|
||||
updatePublicSettings: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export default function ProjectSettings() {
|
||||
export default function ProjectSettingsPage() {
|
||||
const snack = useSnackContext();
|
||||
|
||||
const [settingsState] = useDoc({ path: SETTINGS }, { createIfMissing: true });
|
||||
|
||||
@@ -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<User>(
|
||||
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 (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="Users">TODO:</SettingsSection>
|
||||
<FloatingSearch label="Search Users" onChange={handleChange as any} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="space-between"
|
||||
alignItems="baseline"
|
||||
sx={{ mt: 4, mx: 1, mb: 0.5, cursor: "default" }}
|
||||
>
|
||||
<Typography variant="subtitle1" component="h2">
|
||||
Users
|
||||
</Typography>
|
||||
{!loading && (
|
||||
<Typography variant="button" component="div">
|
||||
{query
|
||||
? `${filteredUsers.length} of ${usersState.documents.length}`
|
||||
: usersState.documents.length}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Paper>
|
||||
<List>
|
||||
{loading || (query === "" && filteredUsers.length === 0) ? (
|
||||
<>
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
<UserSkeleton />
|
||||
</>
|
||||
) : (
|
||||
filteredUsers.map((user) => <UserItem key={user.id} {...user} />)
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user