Upgrade to React-Admin 4

This commit is contained in:
dklimpel 2023-02-04 16:57:37 +01:00
parent 08472937e8
commit 24a6b66203
13 changed files with 483 additions and 1043 deletions

View File

@ -18,22 +18,20 @@
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"prettier": "^2.2.0", "prettier": "^2.2.0"
"ra-test": "^3.15.0"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.7.1", "@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0", "@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.3.1", "@mui/icons-material": "^5.3.1",
"@mui/material": "^5.11.7", "@mui/material": "^5.11.7",
"@mui/styles": "^5.11.7",
"papaparse": "^5.2.0", "papaparse": "^5.2.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"ra-language-chinese": "^2.0.10", "ra-language-chinese": "^2.0.10",
"ra-language-french": "^4.2.0", "ra-language-french": "^4.2.0",
"ra-language-german": "^3.13.4", "ra-language-german": "^3.13.4",
"react": "^17.0.0", "react": "^17.0.0",
"react-admin": "^3.19.7", "react-admin": "^4.7.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "^5.0.1" "react-scripts": "^5.0.1"
}, },

View File

@ -1,5 +1,10 @@
import React from "react"; import React from "react";
import { Admin, Resource, resolveBrowserLocale } from "react-admin"; import {
Admin,
CustomRoutes,
Resource,
resolveBrowserLocale,
} from "react-admin";
import polyglotI18nProvider from "ra-i18n-polyglot"; import polyglotI18nProvider from "ra-i18n-polyglot";
import authProvider from "./synapse/authProvider"; import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider"; import dataProvider from "./synapse/dataProvider";
@ -48,10 +53,10 @@ const App = () => (
authProvider={authProvider} authProvider={authProvider}
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[
<Route key="userImport" path="/import_users" component={ImportFeature} />,
]}
> >
<CustomRoutes>
<Route path="/import_users" element={<ImportFeature />} />
</CustomRoutes>
<Resource <Resource
name="users" name="users"
list={UserList} list={UserList}

View File

@ -1,19 +1,20 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
fetchUtils, fetchUtils,
Form,
FormDataConsumer, FormDataConsumer,
Notification, Notification,
useLogin, useLogin,
useNotify, useNotify,
useLocale, useLocaleState,
useSetLocale,
useTranslate, useTranslate,
PasswordInput, PasswordInput,
TextInput, TextInput,
} from "react-admin"; } from "react-admin";
import { Form, useForm } from "react-final-form"; import { useForm } from "react-hook-form";
import { import {
Avatar, Avatar,
Box,
Button, Button,
Card, Card,
CardActions, CardActions,
@ -22,50 +23,49 @@ import {
Select, Select,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { makeStyles } from "@material-ui/core/styles"; import { styled } from "@mui/material/styles";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
const useStyles = makeStyles(theme => ({ const FormBox = styled("div")(({ theme }) => ({
main: { display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", minHeight: "calc(100vh - 1em)",
minHeight: "calc(100vh - 1em)", alignItems: "center",
alignItems: "center", justifyContent: "flex-start",
justifyContent: "flex-start", background: "url(./images/floating-cogs.svg)",
background: "url(./images/floating-cogs.svg)", backgroundColor: "#f9f9f9",
backgroundColor: "#f9f9f9", backgroundRepeat: "no-repeat",
backgroundRepeat: "no-repeat", backgroundSize: "cover",
backgroundSize: "cover",
}, [`& .card`]: {
card: {
minWidth: "30em", minWidth: "30em",
marginTop: "6em", marginTop: "6em",
marginBottom: "6em", marginBottom: "6em",
}, },
avatar: { [`& .avatar`]: {
margin: "1em", margin: "1em",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
}, },
icon: { [`& .icon`]: {
backgroundColor: theme.palette.secondary.main, backgroundColor: theme.palette.secondary.main,
}, },
hint: { [`& .hint`]: {
marginTop: "1em", marginTop: "1em",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
color: theme.palette.grey[500], color: theme.palette.grey[500],
}, },
form: { [`& .form`]: {
padding: "0 1em 1em 1em", padding: "0 1em 1em 1em",
}, },
input: { [`& .input`]: {
marginTop: "1em", marginTop: "1em",
}, },
actions: { [`& .actions`]: {
padding: "0 1em 1em 1em", padding: "0 1em 1em 1em",
}, },
serverVersion: { [`& .serverVersion`]: {
color: "#9e9e9e", color: "#9e9e9e",
fontFamily: "Roboto, Helvetica, Arial, sans-serif", fontFamily: "Roboto, Helvetica, Arial, sans-serif",
marginBottom: "1em", marginBottom: "1em",
@ -73,14 +73,12 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const LoginPage = ({ theme }) => { const LoginPage = () => {
const classes = useStyles({ theme });
const login = useLogin(); const login = useLogin();
const notify = useNotify(); const notify = useNotify();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [supportPassAuth, setSupportPassAuth] = useState(true); const [supportPassAuth, setSupportPassAuth] = useState(true);
var locale = useLocale(); const [locale, setLocale] = useLocaleState();
const setLocale = useSetLocale();
const translate = useTranslate(); const translate = useTranslate();
const base_url = localStorage.getItem("base_url"); const base_url = localStorage.getItem("base_url");
const cfg_base_url = process.env.REACT_APP_SERVER; const cfg_base_url = process.env.REACT_APP_SERVER;
@ -263,42 +261,39 @@ const LoginPage = ({ theme }) => {
); );
return ( return (
<div> <Box>
<div className={classes.input}> <TextInput
<TextInput autoFocus
autoFocus name="username"
name="username" component={renderInput}
component={renderInput} label="ra.auth.username"
label="ra.auth.username" disabled={loading || !supportPassAuth}
disabled={loading || !supportPassAuth} onBlur={handleUsernameChange}
onBlur={handleUsernameChange} resettable
resettable fullWidth
fullWidth className="input"
/> />
</div> <PasswordInput
<div className={classes.input}> name="password"
<PasswordInput component={renderInput}
name="password" label="ra.auth.password"
component={renderInput} type="password"
label="ra.auth.password" disabled={loading || !supportPassAuth}
type="password" resettable
disabled={loading || !supportPassAuth} fullWidth
resettable className="input"
fullWidth />
/> <TextInput
</div> name="base_url"
<div className={classes.input}> component={renderInput}
<TextInput label="synapseadmin.auth.base_url"
name="base_url" disabled={cfg_base_url || loading}
component={renderInput} resettable
label="synapseadmin.auth.base_url" fullWidth
disabled={cfg_base_url || loading} className="input"
resettable />
fullWidth <Box className="serverVersion">{serverVersion}</Box>
/> </Box>
</div>
<div className={classes.serverVersion}>{serverVersion}</div>
</div>
); );
}; };
@ -307,66 +302,60 @@ const LoginPage = ({ theme }) => {
initialValues={{ base_url: cfg_base_url || base_url }} initialValues={{ base_url: cfg_base_url || base_url }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validate={validate} validate={validate}
render={({ handleSubmit }) => ( >
<form onSubmit={handleSubmit} noValidate> <FormBox>
<div className={classes.main}> <Card className="card">
<Card className={classes.card}> <Box className="avatar">
<div className={classes.avatar}> <Avatar className="icon">
<Avatar className={classes.icon}> <LockIcon />
<LockIcon /> </Avatar>
</Avatar> </Box>
</div> <Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
<div className={classes.hint}> <Box className="form">
{translate("synapseadmin.auth.welcome")} <Select
</div> value={locale}
<div className={classes.form}> onChange={e => {
<div className={classes.input}> setLocale(e.target.value);
<Select }}
value={locale} fullWidth
onChange={e => { disabled={loading}
setLocale(e.target.value); className="input"
}} >
fullWidth <MenuItem value="de">Deutsch</MenuItem>
disabled={loading} <MenuItem value="en">English</MenuItem>
> <MenuItem value="fr">Français</MenuItem>
<MenuItem value="de">Deutsch</MenuItem> <MenuItem value="zh">简体中文</MenuItem>
<MenuItem value="en">English</MenuItem> </Select>
<MenuItem value="fr">Français</MenuItem> <FormDataConsumer>
<MenuItem value="zh">简体中文</MenuItem> {formDataProps => <UserData {...formDataProps} />}
</Select> </FormDataConsumer>
</div> <CardActions className="actions">
<FormDataConsumer> <Button
{formDataProps => <UserData {...formDataProps} />} variant="contained"
</FormDataConsumer> type="submit"
</div> color="primary"
<CardActions className={classes.actions}> disabled={loading || !supportPassAuth}
<Button fullWidth
variant="contained" >
type="submit" {loading && <CircularProgress size={25} thickness={2} />}
color="primary" {translate("ra.auth.sign_in")}
disabled={loading || !supportPassAuth} </Button>
fullWidth <Button
> variant="contained"
{loading && <CircularProgress size={25} thickness={2} />} color="secondary"
{translate("ra.auth.sign_in")} onClick={handleSSO}
</Button> disabled={loading || ssoBaseUrl === ""}
<Button fullWidth
variant="contained" >
color="secondary" {loading && <CircularProgress size={25} thickness={2} />}
onClick={handleSSO} {translate("synapseadmin.auth.sso_sign_in")}
disabled={loading || ssoBaseUrl === ""} </Button>
fullWidth </CardActions>
> </Box>
{loading && <CircularProgress size={25} thickness={2} />} </Card>
{translate("synapseadmin.auth.sso_sign_in")} </FormBox>
</Button> <Notification />
</CardActions> </Form>
</Card>
<Notification />
</div>
</form>
)}
/>
); );
}; };

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import { TestContext } from "ra-test"; import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage"; import LoginPage from "./LoginPage";
describe("LoginForm", () => { describe("LoginForm", () => {
it("renders", () => { it("renders", () => {
render( render(
<TestContext> <AdminContext>
<LoginPage /> <LoginPage />
</TestContext> </AdminContext>
); );
}); });
}); });

View File

@ -1,26 +1,28 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { Avatar, Chip } from "@mui/material"; import { Avatar } from "@mui/material";
import { connect } from "react-redux";
import FolderSharedIcon from "@mui/icons-material/FolderShared"; import FolderSharedIcon from "@mui/icons-material/FolderShared";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
Button, Button,
Datagrid, DatagridConfigurable,
ExportButton,
DeleteButton, DeleteButton,
Filter,
List, List,
NumberField, NumberField,
Pagination, Pagination,
SelectColumnsButton,
TextField, TextField,
TopToolbar,
useCreate, useCreate,
useMutation, useListContext,
useNotify, useNotify,
useTranslate, useTranslate,
useRecordContext, useRecordContext,
useRefresh, useRefresh,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "react-query";
const RoomDirectoryPagination = props => ( const RoomDirectoryPagination = props => (
<Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} /> <Pagination {...props} rowsPerPageOptions={[100, 500, 1000, 2000]} />
@ -59,26 +61,23 @@ export const RoomDirectoryBulkDeleteButton = props => (
/> />
); );
export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => { export const RoomDirectoryBulkSaveButton = () => {
const { selectedIds } = useListContext();
const notify = useNotify(); const notify = useNotify();
const refresh = useRefresh(); const refresh = useRefresh();
const unselectAll = useUnselectAll(); const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation(); const { createMany, isloading } = useMutation();
const handleSend = values => { const handleSend = values => {
createMany( createMany(
["room_directory", "createMany", { ids: selectedIds, data: {} }],
{ {
type: "createMany", onSuccess: data => {
resource: "room_directory",
payload: { ids: selectedIds, data: {} },
},
{
onSuccess: ({ data }) => {
notify("resources.room_directory.action.send_success"); notify("resources.room_directory.action.send_success");
unselectAll("rooms"); unselectAll("rooms");
refresh(); refresh();
}, },
onFailure: error => onError: error =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", {
type: "error", type: "error",
}), }),
@ -90,18 +89,18 @@ export const RoomDirectoryBulkSaveButton = ({ selectedIds }) => {
<Button <Button
label="resources.room_directory.action.create" label="resources.room_directory.action.create"
onClick={handleSend} onClick={handleSend}
disabled={loading} disabled={isloading}
> >
<FolderSharedIcon /> <FolderSharedIcon />
</Button> </Button>
); );
}; };
export const RoomDirectorySaveButton = props => { export const RoomDirectorySaveButton = () => {
const record = useRecordContext(); const record = useRecordContext();
const notify = useNotify(); const notify = useNotify();
const refresh = useRefresh(); const refresh = useRefresh();
const [create, { loading }] = useCreate("room_directory"); const [create, { isloading }] = useCreate("room_directory");
const handleSend = values => { const handleSend = values => {
create( create(
@ -109,11 +108,11 @@ export const RoomDirectorySaveButton = props => {
payload: { data: { id: record.id } }, payload: { data: { id: record.id } },
}, },
{ {
onSuccess: ({ data }) => { onSuccess: data => {
notify("resources.room_directory.action.send_success"); notify("resources.room_directory.action.send_success");
refresh(); refresh();
}, },
onFailure: error => onError: error =>
notify("resources.room_directory.action.send_failure", { notify("resources.room_directory.action.send_failure", {
type: "error", type: "error",
}), }),
@ -125,16 +124,16 @@ export const RoomDirectorySaveButton = props => {
<Button <Button
label="resources.room_directory.action.create" label="resources.room_directory.action.create"
onClick={handleSend} onClick={handleSend}
disabled={loading} disabled={isloading}
> >
<FolderSharedIcon /> <FolderSharedIcon />
</Button> </Button>
); );
}; };
const RoomDirectoryBulkActionButtons = props => ( const RoomDirectoryBulkActionButtons = () => (
<Fragment> <Fragment>
<RoomDirectoryBulkDeleteButton {...props} /> <RoomDirectoryBulkDeleteButton />
</Fragment> </Fragment>
); );
@ -142,110 +141,76 @@ const AvatarField = ({ source, className, record = {} }) => (
<Avatar src={record[source]} className={className} /> <Avatar src={record[source]} className={className} />
); );
const RoomDirectoryFilter = ({ ...props }) => { const RoomDirectoryListActions = () => (
const translate = useTranslate(); <TopToolbar>
return ( <SelectColumnsButton />
<Filter {...props}> <ExportButton />
<Chip </TopToolbar>
label={translate("resources.rooms.fields.room_id")} );
source="room_id"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.topic")}
source="topic"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
<Chip
label={translate("resources.rooms.fields.canonical_alias")}
source="canonical_alias"
defaultValue={false}
sx={{ marginBottom: "8px" }}
/>
</Filter>
);
};
export const FilterableRoomDirectoryList = ({ export const RoomDirectoryList = () => (
roomDirectoryFilters, /*
dispatch,
...props
}) => {
const filter = roomDirectoryFilters; const filter = roomDirectoryFilters;
const roomIdFilter = filter && filter.room_id ? true : false; const roomIdFilter = filter && filter.room_id ? true : false;
const topicFilter = filter && filter.topic ? true : false; const topicFilter = filter && filter.topic ? true : false;
const canonicalAliasFilter = filter && filter.canonical_alias ? true : false; const canonicalAliasFilter = filter && filter.canonical_alias ? true : false;
*/
return ( <List
<List pagination={<RoomDirectoryPagination />}
{...props} perPage={100}
pagination={<RoomDirectoryPagination />} actions={<RoomDirectoryListActions />}
>
<DatagridConfigurable
rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
bulkActionButtons={<RoomDirectoryBulkActionButtons />} bulkActionButtons={<RoomDirectoryBulkActionButtons />}
filters={<RoomDirectoryFilter />} omit={["room_id", "canonical_alias", "topic"]}
perPage={100}
> >
<Datagrid rowClick={(id, basePath, record) => "/rooms/" + id + "/show"}> <AvatarField
<AvatarField source="avatar_src"
source="avatar_src" sortable={false}
sortable={false} sx={{ height: "40px", width: "40px" }}
sx={{ height: "40px", width: "40px" }} label="resources.rooms.fields.avatar"
label="resources.rooms.fields.avatar" />
/> <TextField
<TextField source="name"
source="name" sortable={false}
sortable={false} label="resources.rooms.fields.name"
label="resources.rooms.fields.name" />
/>
{roomIdFilter && (
<TextField
source="room_id"
sortable={false}
label="resources.rooms.fields.room_id"
/>
)}
{canonicalAliasFilter && (
<TextField
source="canonical_alias"
sortable={false}
label="resources.rooms.fields.canonical_alias"
/>
)}
{topicFilter && (
<TextField
source="topic"
sortable={false}
label="resources.rooms.fields.topic"
/>
)}
<NumberField
source="num_joined_members"
sortable={false}
label="resources.rooms.fields.joined_members"
/>
<BooleanField
source="world_readable"
sortable={false}
label="resources.room_directory.fields.world_readable"
/>
<BooleanField
source="guest_can_join"
sortable={false}
label="resources.room_directory.fields.guest_can_join"
/>
</Datagrid>
</List>
);
};
function mapStateToProps(state) { <TextField
return { source="room_id"
roomDirectoryFilters: sortable={false}
state.admin.resources.room_directory.list.params.displayedFilters, label="resources.rooms.fields.room_id"
}; />
}
export const RoomDirectoryList = connect(mapStateToProps)( <TextField
FilterableRoomDirectoryList source="canonical_alias"
sortable={false}
label="resources.rooms.fields.canonical_alias"
/>
<TextField
source="topic"
sortable={false}
label="resources.rooms.fields.topic"
/>
<NumberField
source="num_joined_members"
sortable={false}
label="resources.rooms.fields.joined_members"
/>
<BooleanField
source="world_readable"
sortable={false}
label="resources.room_directory.fields.world_readable"
/>
<BooleanField
source="guest_can_join"
sortable={false}
label="resources.room_directory.fields.guest_can_join"
/>
</DatagridConfigurable>
</List>
); );

View File

@ -7,12 +7,13 @@ import {
Toolbar, Toolbar,
required, required,
useCreate, useCreate,
useMutation, useListContext,
useNotify, useNotify,
useRecordContext, useRecordContext,
useTranslate, useTranslate,
useUnselectAll, useUnselectAll,
} from "react-admin"; } from "react-admin";
import { useMutation } from "react-query";
import MessageIcon from "@mui/icons-material/Message"; import MessageIcon from "@mui/icons-material/Message";
import IconCancel from "@mui/icons-material/Cancel"; import IconCancel from "@mui/icons-material/Cancel";
import { import {
@ -48,7 +49,6 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
</DialogContentText> </DialogContentText>
<SimpleForm <SimpleForm
toolbar={<ServerNoticeToolbar />} toolbar={<ServerNoticeToolbar />}
submitOnEnter={false}
redirect={false} redirect={false}
save={onSend} save={onSend}
> >
@ -67,11 +67,11 @@ const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
); );
}; };
export const ServerNoticeButton = props => { export const ServerNoticeButton = () => {
const record = useRecordContext(); const record = useRecordContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const [create, { loading }] = useCreate("servernotices"); const [create, { isloading }] = useCreate("servernotices");
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
@ -84,7 +84,7 @@ export const ServerNoticeButton = props => {
notify("resources.servernotices.action.send_success"); notify("resources.servernotices.action.send_success");
handleDialogClose(); handleDialogClose();
}, },
onFailure: () => onError: () =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", {
type: "error", type: "error",
}), }),
@ -97,7 +97,7 @@ export const ServerNoticeButton = props => {
<Button <Button
label="resources.servernotices.send" label="resources.servernotices.send"
onClick={handleDialogOpen} onClick={handleDialogOpen}
disabled={loading} disabled={isloading}
> >
<MessageIcon /> <MessageIcon />
</Button> </Button>
@ -110,29 +110,26 @@ export const ServerNoticeButton = props => {
); );
}; };
export const ServerNoticeBulkButton = ({ selectedIds }) => { export const ServerNoticeBulkButton = () => {
const { selectedIds } = useListContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const unselectAll = useUnselectAll(); const unselectAll = useUnselectAll();
const [createMany, { loading }] = useMutation(); const { createMany, isloading } = useMutation();
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
const handleSend = values => { const handleSend = values => {
createMany( createMany(
["servernotices", "createMany", { ids: selectedIds, data: values }],
{ {
type: "createMany", onSuccess: data => {
resource: "servernotices",
payload: { ids: selectedIds, data: values },
},
{
onSuccess: ({ data }) => {
notify("resources.servernotices.action.send_success"); notify("resources.servernotices.action.send_success");
unselectAll("users"); unselectAll("users");
handleDialogClose(); handleDialogClose();
}, },
onFailure: error => onError: error =>
notify("resources.servernotices.action.send_failure", { notify("resources.servernotices.action.send_failure", {
type: "error", type: "error",
}), }),
@ -141,11 +138,11 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
}; };
return ( return (
<Fragment> <>
<Button <Button
label="resources.servernotices.send" label="resources.servernotices.send"
onClick={handleDialogOpen} onClick={handleDialogOpen}
disabled={loading} disabled={isloading}
> >
<MessageIcon /> <MessageIcon />
</Button> </Button>
@ -154,6 +151,6 @@ export const ServerNoticeBulkButton = ({ selectedIds }) => {
onClose={handleDialogClose} onClose={handleDialogClose}
onSend={handleSend} onSend={handleSend}
/> />
</Fragment> </>
); );
}; };

View File

@ -71,7 +71,7 @@ export const DestinationReconnectButton = props => {
}); });
refresh(); refresh();
}, },
onFailure: () => { onError: () => {
notify("ra.message.error", { type: "error" }); notify("ra.message.error", { type: "error" });
}, },
} }
@ -112,11 +112,11 @@ export const DestinationList = props => {
filters={<DestinationFilter />} filters={<DestinationFilter />}
pagination={<DestinationPagination />} pagination={<DestinationPagination />}
sort={{ field: "destination", order: "ASC" }} sort={{ field: "destination", order: "ASC" }}
bulkActionButtons={false}
> >
<Datagrid <Datagrid
rowStyle={destinationRowStyle} rowStyle={destinationRowStyle}
rowClick={(id, basePath, record) => `${basePath}/${id}/show/rooms`} rowClick={(id, resource, record) => `${id}/show/rooms`}
bulkActionButtons={false}
> >
<TextField source="destination" /> <TextField source="destination" />
<DateField source="failure_ts" showTime options={date_format} /> <DateField source="failure_ts" showTime options={date_format} />
@ -160,7 +160,7 @@ export const DestinationShow = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => `/rooms/${id}/show`} rowClick={(id, resource, record) => `/rooms/${id}/show`}
> >
<TextField <TextField
source="room_id" source="room_id"

View File

@ -7,30 +7,12 @@ import {
useRecordContext, useRecordContext,
useRefresh, useRefresh,
} from "react-admin"; } from "react-admin";
import { alpha, useTheme } from "@mui/material/styles";
import ActionDelete from "@mui/icons-material/Delete"; import ActionDelete from "@mui/icons-material/Delete";
import { makeStyles } from "@material-ui/core/styles";
import { alpha } from "@mui/material/styles";
import classnames from "classnames";
const useStyles = makeStyles(
theme => ({
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
},
}),
{ name: "RaDeleteDeviceButton" }
);
export const DeviceRemoveButton = props => { export const DeviceRemoveButton = props => {
const theme = useTheme();
const record = useRecordContext(); const record = useRecordContext();
const classes = useStyles(props);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const refresh = useRefresh(); const refresh = useRefresh();
const notify = useNotify(); const notify = useNotify();
@ -63,7 +45,16 @@ export const DeviceRemoveButton = props => {
<Button <Button
label="ra.action.remove" label="ra.action.remove"
onClick={handleClick} onClick={handleClick}
className={classnames("ra-delete-button", classes.deleteButton)} sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
}}
> >
<ActionDelete /> <ActionDelete />
</Button> </Button>

View File

@ -1,7 +1,4 @@
import React, { Fragment, useState } from "react"; import React, { Fragment, useState } from "react";
import classnames from "classnames";
import { alpha } from "@mui/material/styles";
import { makeStyles } from "@material-ui/core/styles";
import { import {
BooleanInput, BooleanInput,
Button, Button,
@ -17,6 +14,7 @@ import {
useRefresh, useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import { alpha, useTheme } from "@mui/material/styles";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import DeleteSweepIcon from "@mui/icons-material/DeleteSweep";
@ -31,22 +29,6 @@ import IconCancel from "@mui/icons-material/Cancel";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import LockOpenIcon from "@mui/icons-material/LockOpen"; import LockOpenIcon from "@mui/icons-material/LockOpen";
const useStyles = makeStyles(
theme => ({
deleteButton: {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
},
}),
{ name: "RaDeleteDeviceButton" }
);
const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => { const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
const translate = useTranslate(); const translate = useTranslate();
@ -81,7 +63,6 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
</DialogContentText> </DialogContentText>
<SimpleForm <SimpleForm
toolbar={<DeleteMediaToolbar />} toolbar={<DeleteMediaToolbar />}
submitOnEnter={false}
redirect={false} redirect={false}
save={onSend} save={onSend}
> >
@ -113,10 +94,10 @@ const DeleteMediaDialog = ({ open, loading, onClose, onSend }) => {
}; };
export const DeleteMediaButton = props => { export const DeleteMediaButton = props => {
const classes = useStyles(props); const theme = useTheme();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const notify = useNotify(); const notify = useNotify();
const [deleteOne, { loading }] = useDelete("delete_media"); const [deleteOne, { isLoading }] = useDelete("delete_media");
const handleDialogOpen = () => setOpen(true); const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false); const handleDialogClose = () => setOpen(false);
@ -129,7 +110,7 @@ export const DeleteMediaButton = props => {
notify("resources.delete_media.action.send_success"); notify("resources.delete_media.action.send_success");
handleDialogClose(); handleDialogClose();
}, },
onFailure: () => onError: () =>
notify("resources.delete_media.action.send_failure", { notify("resources.delete_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -142,8 +123,17 @@ export const DeleteMediaButton = props => {
<Button <Button
label="resources.delete_media.action.send" label="resources.delete_media.action.send"
onClick={handleDialogOpen} onClick={handleDialogOpen}
disabled={loading} disabled={isLoading}
className={classnames("ra-delete-button", classes.deleteButton)} sx={{
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, 0.12),
// Reset on mouse devices
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
}}
> >
<DeleteSweepIcon /> <DeleteSweepIcon />
</Button> </Button>
@ -174,7 +164,7 @@ export const ProtectMediaButton = props => {
notify("resources.protect_media.action.send_success"); notify("resources.protect_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.protect_media.action.send_failure", { notify("resources.protect_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -190,7 +180,7 @@ export const ProtectMediaButton = props => {
notify("resources.protect_media.action.send_success"); notify("resources.protect_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.protect_media.action.send_failure", { notify("resources.protect_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -270,7 +260,7 @@ export const QuarantineMediaButton = props => {
notify("resources.quarantine_media.action.send_success"); notify("resources.quarantine_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.quarantine_media.action.send_failure", { notify("resources.quarantine_media.action.send_failure", {
type: "error", type: "error",
}), }),
@ -286,7 +276,7 @@ export const QuarantineMediaButton = props => {
notify("resources.quarantine_media.action.send_success"); notify("resources.quarantine_media.action.send_success");
refresh(); refresh();
}, },
onFailure: () => onError: () =>
notify("resources.quarantine_media.action.send_failure", { notify("resources.quarantine_media.action.send_failure", {
type: "error", type: "error",
}), }),

View File

@ -1,11 +1,12 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { connect } from "react-redux";
import { import {
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
DateField, DateField,
Datagrid, Datagrid,
DatagridConfigurable,
DeleteButton, DeleteButton,
ExportButton,
Filter, Filter,
List, List,
NumberField, NumberField,
@ -13,6 +14,7 @@ import {
ReferenceField, ReferenceField,
ReferenceManyField, ReferenceManyField,
SearchInput, SearchInput,
SelectColumnsButton,
SelectField, SelectField,
Show, Show,
Tab, Tab,
@ -54,8 +56,9 @@ const RoomPagination = props => (
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} /> <Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
); );
const EncryptionField = ({ source, record = {}, emptyText }) => { const EncryptionField = ({ source, emptyText }) => {
const translate = useTranslate(); const translate = useTranslate();
const record = useRecordContext();
const value = get(record, source); const value = get(record, source);
let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true"; let ariaLabel = value === false ? "ra.boolean.false" : "ra.boolean.true";
@ -95,7 +98,7 @@ const RoomTitle = props => {
); );
}; };
const RoomShowActions = ({ basePath, data, resource }) => { const RoomShowActions = ({ data, resource }) => {
var roomDirectoryStatus = ""; var roomDirectoryStatus = "";
if (data) { if (data) {
roomDirectoryStatus = data.public; roomDirectoryStatus = data.public;
@ -110,7 +113,6 @@ const RoomShowActions = ({ basePath, data, resource }) => {
<RoomDirectoryDeleteButton record={data} /> <RoomDirectoryDeleteButton record={data} />
)} )}
<DeleteButton <DeleteButton
basePath={basePath}
record={data} record={data}
resource={resource} resource={resource}
mutationMode="pessimistic" mutationMode="pessimistic"
@ -163,7 +165,7 @@ export const RoomShow = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/users/" + id} rowClick={(id, resource, record) => "/users/" + id}
> >
<TextField <TextField
source="id" source="id"
@ -304,12 +306,11 @@ export const RoomShow = props => {
); );
}; };
const RoomBulkActionButtons = props => ( const RoomBulkActionButtons = () => (
<Fragment> <Fragment>
<RoomDirectoryBulkSaveButton {...props} /> <RoomDirectoryBulkSaveButton />
<RoomDirectoryBulkDeleteButton {...props} /> <RoomDirectoryBulkDeleteButton />
<BulkDeleteButton <BulkDeleteButton
{...props}
confirmTitle="resources.rooms.action.erase.title" confirmTitle="resources.rooms.action.erase.title"
confirmContent="resources.rooms.action.erase.content" confirmContent="resources.rooms.action.erase.content"
mutationMode="pessimistic" mutationMode="pessimistic"
@ -364,44 +365,38 @@ RoomNameField.propTypes = {
source: PropTypes.string.isRequired, source: PropTypes.string.isRequired,
}; };
const FilterableRoomList = ({ roomFilters, dispatch, ...props }) => { const RoomListActions = () => (
const filter = roomFilters; <TopToolbar>
const localMembersFilter = <SelectColumnsButton />
filter && filter.joined_local_members ? true : false; <ExportButton />
const stateEventsFilter = filter && filter.state_events ? true : false; </TopToolbar>
const versionFilter = filter && filter.version ? true : false; );
const federateableFilter = filter && filter.federatable ? true : false;
return ( export const RoomList = () => (
<List <List
{...props} pagination={<RoomPagination />}
pagination={<RoomPagination />} sort={{ field: "name", order: "ASC" }}
sort={{ field: "name", order: "ASC" }} filters={<RoomFilter />}
filters={<RoomFilter />} actions={<RoomListActions />}
>
<DatagridConfigurable
rowClick="show"
bulkActionButtons={<RoomBulkActionButtons />} bulkActionButtons={<RoomBulkActionButtons />}
omit={["joined_local_members", "state_events", "version", "federatable"]}
> >
<Datagrid rowClick="show"> <EncryptionField
<EncryptionField source="is_encrypted"
source="is_encrypted" sortBy="encryption"
sortBy="encryption" //label={<HttpsIcon />}
label={<HttpsIcon />} />
/>
<RoomNameField source="name" />
<TextField source="joined_members" />
{localMembersFilter && <TextField source="joined_local_members" />}
{stateEventsFilter && <TextField source="state_events" />}
{versionFilter && <TextField source="version" />}
{federateableFilter && <BooleanField source="federatable" />}
<BooleanField source="public" />
</Datagrid>
</List>
);
};
function mapStateToProps(state) { <RoomNameField source="name" />
return { <TextField source="joined_members" />
roomFilters: state.admin.resources.rooms.list.params.displayedFilters, <TextField source="joined_local_members" />
}; <TextField source="state_events" />
} <TextField source="version" />
<BooleanField source="federatable" />
export const RoomList = connect(mapStateToProps)(FilterableRoomList); <BooleanField source="public" />
</DatagridConfigurable>
</List>
);

View File

@ -65,9 +65,11 @@ export const UserMediaStatsList = props => {
filters={<UserMediaStatsFilter />} filters={<UserMediaStatsFilter />}
pagination={<UserMediaStatsPagination />} pagination={<UserMediaStatsPagination />}
sort={{ field: "media_length", order: "DESC" }} sort={{ field: "media_length", order: "DESC" }}
bulkActionButtons={false}
> >
<Datagrid rowClick={(id, basePath, record) => "/users/" + id + "/media"}> <Datagrid
rowClick={(id, resource, record) => "/users/" + id + "/media"}
bulkActionButtons={false}
>
<TextField source="user_id" label="resources.users.fields.id" /> <TextField source="user_id" label="resources.users.fields.id" />
<TextField <TextField
source="displayname" source="displayname"

View File

@ -53,12 +53,6 @@ import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices"; import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media"; import { ProtectMediaButton, QuarantineMediaButton } from "./media";
const redirect = () => {
return {
pathname: "/import_users",
};
};
const choices_medium = [ const choices_medium = [
{ id: "email", name: "resources.users.email" }, { id: "email", name: "resources.users.email" },
{ id: "msisdn", name: "resources.users.msisdn" }, { id: "msisdn", name: "resources.users.msisdn" },
@ -88,7 +82,6 @@ const UserListActions = ({
filterValues, filterValues,
permanentFilter, permanentFilter,
hasCreate, // you can hide CreateButton if hasCreate = false hasCreate, // you can hide CreateButton if hasCreate = false
basePath,
selectedIds, selectedIds,
onUnselectItems, onUnselectItems,
showFilter, showFilter,
@ -106,7 +99,7 @@ const UserListActions = ({
filterValues, filterValues,
context: "button", context: "button",
})} })}
<CreateButton basePath={basePath} /> <CreateButton />
<ExportButton <ExportButton
disabled={total === 0} disabled={total === 0}
resource={resource} resource={resource}
@ -116,7 +109,7 @@ const UserListActions = ({
maxResults={maxResults} maxResults={maxResults}
/> />
{/* Add your custom actions */} {/* Add your custom actions */}
<Button component={Link} to={redirect} label="CSV Import"> <Button component={Link} to="/import_users" label="CSV Import">
<GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} /> <GetAppIcon sx={{ transform: "rotate(180deg)", fontSize: "20px" }} />
</Button> </Button>
</TopToolbar> </TopToolbar>
@ -144,16 +137,15 @@ const UserFilter = props => (
</Filter> </Filter>
); );
const UserBulkActionButtons = props => ( const UserBulkActionButtons = () => (
<Fragment> <>
<ServerNoticeBulkButton {...props} /> <ServerNoticeBulkButton />
<BulkDeleteButton <BulkDeleteButton
{...props}
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</Fragment> </>
); );
const AvatarField = ({ source, record = {}, sx }) => ( const AvatarField = ({ source, record = {}, sx }) => (
@ -168,10 +160,9 @@ export const UserList = props => {
filterDefaultValues={{ guests: true, deactivated: false }} filterDefaultValues={{ guests: true, deactivated: false }}
sort={{ field: "name", order: "ASC" }} sort={{ field: "name", order: "ASC" }}
actions={<UserListActions maxResults={10000} />} actions={<UserListActions maxResults={10000} />}
bulkActionButtons={<UserBulkActionButtons />}
pagination={<UserPagination />} pagination={<UserPagination />}
> >
<Datagrid rowClick="edit"> <Datagrid rowClick="edit" bulkActionButtons={<UserBulkActionButtons />}>
<AvatarField <AvatarField
source="avatar_src" source="avatar_src"
sx={{ height: "40px", width: "40px" }} sx={{ height: "40px", width: "40px" }}
@ -248,7 +239,7 @@ export function generateRandomUser() {
const UserEditToolbar = props => ( const UserEditToolbar = props => (
<Toolbar {...props}> <Toolbar {...props}>
<SaveButton submitOnEnter={true} disabled={props.pristine} /> <SaveButton disabled={props.pristine} />
</Toolbar> </Toolbar>
); );
@ -288,7 +279,6 @@ export const UserCreate = props => (
source="user_type" source="user_type"
choices={choices_type} choices={choices_type}
translateChoice={false} translateChoice={false}
allowEmpty={true}
resettable resettable
/> />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
@ -354,7 +344,6 @@ export const UserEdit = props => {
source="user_type" source="user_type"
choices={choices_type} choices={choices_type}
translateChoice={false} translateChoice={false}
allowEmpty={true}
resettable resettable
/> />
<BooleanInput source="admin" /> <BooleanInput source="admin" />
@ -498,7 +487,7 @@ export const UserEdit = props => {
> >
<Datagrid <Datagrid
style={{ width: "100%" }} style={{ width: "100%" }}
rowClick={(id, basePath, record) => "/rooms/" + id + "/show"} rowClick={(id, resource, record) => "/rooms/" + id + "/show"}
> >
<TextField <TextField
source="id" source="id"

819
yarn.lock

File diff suppressed because it is too large Load Diff