Skip to content

Commit

Permalink
Expose user avatar URL field in the UI (#27)
Browse files Browse the repository at this point in the history
* wip

* some fixes

* update readme

* update readme

* Add option to change/erase any user's avatar

* Fix README

* Remove mutationMode from Edit

* remove log

* update readme
  • Loading branch information
aine-etke authored Sep 17, 2024
1 parent d5113aa commit 24cf0a6
Show file tree
Hide file tree
Showing 13 changed files with 138 additions and 78 deletions.
16 changes: 0 additions & 16 deletions .github/CONTRIBUTING.md

This file was deleted.

18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
[![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)
[![Build Status](https://api.travis-ci.com/Awesome-Technologies/synapse-admin.svg?branch=master)](https://app.travis-ci.com/github/Awesome-Technologies/synapse-admin)
[![build-test](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/build-test.yml)
[![gh-pages](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/edge_ghpage.yml/badge.svg)](https://awesome-technologies.github.io/synapse-admin/)
[![docker-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/docker-release.yml/badge.svg)](https://hub.docker.com/r/awesometechnologies/synapse-admin)
[![github-release](https://github.com/Awesome-Technologies/synapse-admin/actions/workflows/github-release.yml/badge.svg)](https://github.com/Awesome-Technologies/synapse-admin/releases)

# Synapse admin ui
# Synapse Admin UI [![GitHub license](https://img.shields.io/github/license/Awesome-Technologies/synapse-admin)](https://github.com/Awesome-Technologies/synapse-admin/blob/master/LICENSE)

This project is built using [react-admin](https://marmelab.com/react-admin/).

## Fork differences

With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
user-friendly interface for managing Synapse homeservers.

### Available via CDN

On [admin.etke.cc](https://admin.etke.cc) you can find the latest version of this fork.

### Changes

With [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin) as the upstream, this
fork is intended to be a more feature-rich version of the original project. The main goal is to provide a more
user-friendly interface for managing Synapse homeservers.

The following changes are already implemented:

* [Prevent admins from deleting themselves](https://github.com/etkecc/synapse-admin/pull/1)
Expand All @@ -38,6 +31,7 @@ The following changes are already implemented:
* [Add UI option to block deleted rooms from being rejoined](https://github.com/etkecc/synapse-admin/pull/26)
* [Fix required fields check on Bulk registration CSV upload](https://github.com/etkecc/synapse-admin/pull/32)
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)

_the list will be updated as new changes are added_

Expand Down
1 change: 1 addition & 0 deletions src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const de: SynapseTranslationMessages = {
},
action: {
erase: "Lösche Benutzerdaten",
erase_avatar: "Avatar löschen"
},
},
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const en: SynapseTranslationMessages = {
},
action: {
erase: "Erase user data",
erase_avatar: "Erase avatar"
},
},
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ const fr: SynapseTranslationMessages = {
},
action: {
erase: "Effacer les données de l'utilisateur",
erase_avatar: "Effacer l'avatar",
},
},
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
};
action: {
erase: string;
erase_avatar: string;
};
};
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ const ru: SynapseTranslationMessages = {
},
action: {
erase: "Удалить данные пользователя",
erase_avatar: "Удалить аватар",
},
},
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const zh: SynapseTranslationMessages = {
},
action: {
erase: "抹除用户信息",
erase_avatar: "抹掉头像",
},
},
rooms: {
Expand Down
81 changes: 47 additions & 34 deletions src/resources/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {
ToolbarClasses,
Identifier,
RaRecord,
ImageInput,
ImageField,
} from "react-admin";
import { Link } from "react-router-dom";

Expand Down Expand Up @@ -101,46 +103,45 @@ const userFilters = [
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
];

const UserPreventSelfDelete: React.FC<{ children: React.ReactNode, ownUserIsSelected: boolean }> = (props) => {
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean }> = props => {
const ownUserIsSelected = props.ownUserIsSelected;
const notify = useNotify();
const translate = useTranslate();

const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
if (ownUserIsSelected) {
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>)
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
ev.stopPropagation();
}
};

return <div onClickCapture={handleDeleteClick}>
{props.children}
</div>
return <div onClickCapture={handleDeleteClick}>{props.children}</div>;
};

const UserBulkActionButtons = () => {
const record = useListContext();
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
const selectedIds = record.selectedIds;
const ownUserId = localStorage.getItem("user_id");
const notify = useNotify();
const translate = useTranslate();

useEffect(() => {
setOwnUserIsSelected(selectedIds.includes(ownUserId));
}, [ selectedIds ]);
}, [selectedIds]);


return <>
<ServerNoticeBulkButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BulkDeleteButton
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
</UserPreventSelfDelete>
</>
return (
<>
<ServerNoticeBulkButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BulkDeleteButton
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
</UserPreventSelfDelete>
</>
);
};

const usersRowClick = (id: Identifier, resource: string, record: RaRecord): string => {
Expand Down Expand Up @@ -204,9 +205,12 @@ const UserEditActions = () => {
};

export const UserCreate = (props: CreateProps) => (
<Create { ...props} redirect={(resource, id, data) => {
return `users/${id}`;
}}>
<Create
{...props}
redirect={(resource, id, data) => {
return `users/${id}`;
}}
>
<SimpleForm>
<TextInput source="id" autoComplete="off" validate={validateUser} />
<TextInput source="displayname" validate={maxLength(256)} />
Expand Down Expand Up @@ -237,7 +241,7 @@ const UserTitle = () => {
{translate("resources.users.name", {
smart_count: 1,
})}{" "}
{record ? ( record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
</span>
);
};
Expand All @@ -250,29 +254,34 @@ const UserEditToolbar = () => {
ownUserIsSelected = record.id === ownUserId;
}

return <>
<div className={ToolbarClasses.defaultToolbar}>
<Toolbar sx={{ justifyContent: "space-between" }}>
return (
<>
<div className={ToolbarClasses.defaultToolbar}>
<Toolbar sx={{ justifyContent: "space-between" }}>
<SaveButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<DeleteButton />
</UserPreventSelfDelete>
</Toolbar>
</div>
</>
</Toolbar>
</div>
</>
);
};

const UserBooleanInput = (props) => {
const UserBooleanInput = props => {
const record = useRecordContext();
const ownUserId = localStorage.getItem("user_id");
const isOwnUser = false;
let ownUserIsSelected = false;
if (record && (record.id === ownUserId)) {
if (record && record.id === ownUserId) {
ownUserIsSelected = true;
}

return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
}
return (
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BooleanInput {...props} disabled={ownUserIsSelected} />
</UserPreventSelfDelete>
);
};

export const UserEdit = (props: EditProps) => {
const translate = useTranslate();
Expand All @@ -281,7 +290,11 @@ export const UserEdit = (props: EditProps) => {
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px" }} />
<BooleanInput source="avatar_erase" label="resources.users.action.erase_avatar" />
<ImageInput source="avatar_file" label="resources.users.fields.avatar" accept="image/*">
<ImageField source="src" title="Avatar" />
</ImageInput>
<TextInput source="id" disabled />
<TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
Expand Down
2 changes: 1 addition & 1 deletion src/synapse/authProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("authProvider", () => {
});

it("should reject if error.status is 401", async () => {
await expect(authProvider.checkError({ status: 401 })).rejects.toBeUndefined();
await expect(authProvider.checkError(new HttpError("test-error", 401, {errcode: "test-errcode", error: "test-error"}))).rejects.toBeDefined();
});

it("should reject if error.status is 403", async () => {
Expand Down
12 changes: 6 additions & 6 deletions src/synapse/dataProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe("dataProvider", () => {
JSON.stringify({
users: [
{
name: "user_id1",
name: "@user_id1:provider",
password_hash: "password_hash1",
is_guest: 0,
admin: 0,
Expand All @@ -27,7 +27,7 @@ describe("dataProvider", () => {
displayname: "User One",
},
{
name: "user_id2",
name: "@user_id2:provider",
password_hash: "password_hash2",
is_guest: 0,
admin: 1,
Expand All @@ -47,15 +47,15 @@ describe("dataProvider", () => {
filter: { author_id: 12 },
});

expect(users.data[0].id).toEqual("user_id1");
expect(users.data[0].id).toEqual("@user_id1:provider");
expect(users.total).toEqual(200);
expect(fetch).toHaveBeenCalledTimes(1);
});

it("fetches one user", async () => {
fetchMock.mockResponseOnce(
JSON.stringify({
name: "user_id1",
name: "@user_id1:provider",
password: "user_password",
displayname: "User",
threepids: [
Expand All @@ -74,9 +74,9 @@ describe("dataProvider", () => {
})
);

const user = await dataProvider.getOne("users", { id: "user_id1" });
const user = await dataProvider.getOne("users", { id: "@user_id1:provider" });

expect(user.data.id).toEqual("user_id1");
expect(user.data.id).toEqual("@user_id1:provider");
expect(user.data.displayname).toEqual("User");
expect(fetch).toHaveBeenCalledTimes(1);
});
Expand Down
Loading

0 comments on commit 24cf0a6

Please sign in to comment.