Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose user avatar URL field in the UI #27

Merged
merged 10 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading