feat: ability to edit user settings in bulk (#597)
parent
2f75c4c6ae
commit
4b0241c3b3
@ -0,0 +1,157 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PermissionOption, { PermissionItem } from '../PermissionOption';
|
||||||
|
import { Permission, User } from '../../hooks/useUser';
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
admin: 'Admin',
|
||||||
|
adminDescription:
|
||||||
|
'Full administrator access. Bypasses all permission checks.',
|
||||||
|
users: 'Manage Users',
|
||||||
|
usersDescription:
|
||||||
|
'Grants permission to manage Overseerr users. Users with this permission cannot modify users with Administrator privilege, or grant it.',
|
||||||
|
settings: 'Manage Settings',
|
||||||
|
settingsDescription:
|
||||||
|
'Grants permission to modify all Overseerr settings. A user must have this permission to grant it to others.',
|
||||||
|
managerequests: 'Manage Requests',
|
||||||
|
managerequestsDescription:
|
||||||
|
'Grants permission to manage Overseerr requests. This includes approving and denying requests.',
|
||||||
|
request: 'Request',
|
||||||
|
requestDescription: 'Grants permission to request movies and series.',
|
||||||
|
vote: 'Vote',
|
||||||
|
voteDescription:
|
||||||
|
'Grants permission to vote on requests (voting not yet implemented)',
|
||||||
|
autoapprove: 'Auto Approve',
|
||||||
|
autoapproveDescription:
|
||||||
|
'Grants auto approval for any requests made by this user.',
|
||||||
|
autoapproveMovies: 'Auto Approve Movies',
|
||||||
|
autoapproveMoviesDescription:
|
||||||
|
'Grants auto approve for movie requests made by this user.',
|
||||||
|
autoapproveSeries: 'Auto Approve Series',
|
||||||
|
autoapproveSeriesDescription:
|
||||||
|
'Grants auto approve for series requests made by this user.',
|
||||||
|
request4k: 'Request 4K',
|
||||||
|
request4kDescription: 'Grants permission to request 4K movies and series.',
|
||||||
|
request4kMovies: 'Request 4K Movies',
|
||||||
|
request4kMoviesDescription: 'Grants permission to request 4K movies.',
|
||||||
|
request4kTv: 'Request 4K Series',
|
||||||
|
request4kTvDescription: 'Grants permission to request 4K Series.',
|
||||||
|
advancedrequest: 'Advanced Requests',
|
||||||
|
advancedrequestDescription:
|
||||||
|
'Grants permission to use advanced request options. (Ex. Changing servers/profiles/paths)',
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PermissionEditProps {
|
||||||
|
currentPermission: number;
|
||||||
|
user?: User;
|
||||||
|
onUpdate: (newPermissions: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionEdit: React.FC<PermissionEditProps> = ({
|
||||||
|
currentPermission,
|
||||||
|
onUpdate,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const permissionList: PermissionItem[] = [
|
||||||
|
{
|
||||||
|
id: 'admin',
|
||||||
|
name: intl.formatMessage(messages.admin),
|
||||||
|
description: intl.formatMessage(messages.adminDescription),
|
||||||
|
permission: Permission.ADMIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
name: intl.formatMessage(messages.settings),
|
||||||
|
description: intl.formatMessage(messages.settingsDescription),
|
||||||
|
permission: Permission.MANAGE_SETTINGS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'users',
|
||||||
|
name: intl.formatMessage(messages.users),
|
||||||
|
description: intl.formatMessage(messages.usersDescription),
|
||||||
|
permission: Permission.MANAGE_USERS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'managerequest',
|
||||||
|
name: intl.formatMessage(messages.managerequests),
|
||||||
|
description: intl.formatMessage(messages.managerequestsDescription),
|
||||||
|
permission: Permission.MANAGE_REQUESTS,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'advancedrequest',
|
||||||
|
name: intl.formatMessage(messages.advancedrequest),
|
||||||
|
description: intl.formatMessage(messages.advancedrequestDescription),
|
||||||
|
permission: Permission.REQUEST_ADVANCED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request',
|
||||||
|
name: intl.formatMessage(messages.request),
|
||||||
|
description: intl.formatMessage(messages.requestDescription),
|
||||||
|
permission: Permission.REQUEST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request4k',
|
||||||
|
name: intl.formatMessage(messages.request4k),
|
||||||
|
description: intl.formatMessage(messages.request4kDescription),
|
||||||
|
permission: Permission.REQUEST_4K,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'request4k-movies',
|
||||||
|
name: intl.formatMessage(messages.request4kMovies),
|
||||||
|
description: intl.formatMessage(messages.request4kMoviesDescription),
|
||||||
|
permission: Permission.REQUEST_4K_MOVIE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'request4k-tv',
|
||||||
|
name: intl.formatMessage(messages.request4kTv),
|
||||||
|
description: intl.formatMessage(messages.request4kTvDescription),
|
||||||
|
permission: Permission.REQUEST_4K_TV,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'autoapprove',
|
||||||
|
name: intl.formatMessage(messages.autoapprove),
|
||||||
|
description: intl.formatMessage(messages.autoapproveDescription),
|
||||||
|
permission: Permission.AUTO_APPROVE,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'autoapprovemovies',
|
||||||
|
name: intl.formatMessage(messages.autoapproveMovies),
|
||||||
|
description: intl.formatMessage(
|
||||||
|
messages.autoapproveMoviesDescription
|
||||||
|
),
|
||||||
|
permission: Permission.AUTO_APPROVE_MOVIE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'autoapprovetv',
|
||||||
|
name: intl.formatMessage(messages.autoapproveSeries),
|
||||||
|
description: intl.formatMessage(
|
||||||
|
messages.autoapproveSeriesDescription
|
||||||
|
),
|
||||||
|
permission: Permission.AUTO_APPROVE_TV,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{permissionList.map((permissionItem) => (
|
||||||
|
<PermissionOption
|
||||||
|
key={`permission-option-${permissionItem.id}`}
|
||||||
|
option={permissionItem}
|
||||||
|
user={user}
|
||||||
|
currentPermission={currentPermission}
|
||||||
|
onUpdate={(newPermission) => onUpdate(newPermission)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionEdit;
|
@ -0,0 +1,115 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PermissionEdit from '../PermissionEdit';
|
||||||
|
import Modal from '../Common/Modal';
|
||||||
|
import { User, useUser } from '../../hooks/useUser';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
import { messages as userEditMessages } from '../UserEdit';
|
||||||
|
|
||||||
|
interface BulkEditProps {
|
||||||
|
selectedUserIds: number[];
|
||||||
|
users?: User[];
|
||||||
|
onCancel?: () => void;
|
||||||
|
onComplete?: (updatedUsers: User[]) => void;
|
||||||
|
onSaving?: (isSaving: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
userssaved: 'Users saved',
|
||||||
|
});
|
||||||
|
|
||||||
|
const BulkEditModal: React.FC<BulkEditProps> = ({
|
||||||
|
selectedUserIds,
|
||||||
|
users,
|
||||||
|
onCancel,
|
||||||
|
onComplete,
|
||||||
|
onSaving,
|
||||||
|
}) => {
|
||||||
|
const { user: currentUser } = useUser();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const [currentPermission, setCurrentPermission] = useState(0);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onSaving) {
|
||||||
|
onSaving(isSaving);
|
||||||
|
}
|
||||||
|
}, [isSaving, onSaving]);
|
||||||
|
|
||||||
|
const updateUsers = async () => {
|
||||||
|
try {
|
||||||
|
setIsSaving(true);
|
||||||
|
const { data: updated } = await axios.put<User[]>(`/api/v1/user`, {
|
||||||
|
ids: selectedUserIds,
|
||||||
|
permissions: currentPermission,
|
||||||
|
});
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(updated);
|
||||||
|
}
|
||||||
|
addToast(intl.formatMessage(messages.userssaved), {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addToast(intl.formatMessage(userEditMessages.userfail), {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (users) {
|
||||||
|
const selectedUsers = users.filter((u) => selectedUserIds.includes(u.id));
|
||||||
|
const { permissions: allPermissionsEqual } = selectedUsers.reduce(
|
||||||
|
({ permissions: aPerms }, { permissions: bPerms }) => {
|
||||||
|
return {
|
||||||
|
permissions: aPerms === bPerms ? aPerms : NaN,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ permissions: selectedUsers[0].permissions }
|
||||||
|
);
|
||||||
|
if (allPermissionsEqual) {
|
||||||
|
setCurrentPermission(allPermissionsEqual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [users, selectedUserIds]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage(userEditMessages.edituser)}
|
||||||
|
onOk={() => {
|
||||||
|
updateUsers();
|
||||||
|
}}
|
||||||
|
okDisabled={isSaving}
|
||||||
|
okText={intl.formatMessage(userEditMessages.save)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-baseline">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="text-base font-medium leading-6 sm:text-sm sm:leading-5"
|
||||||
|
id="label-permissions"
|
||||||
|
>
|
||||||
|
<FormattedMessage {...userEditMessages.permissions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg">
|
||||||
|
<PermissionEdit
|
||||||
|
user={currentUser}
|
||||||
|
currentPermission={currentPermission}
|
||||||
|
onUpdate={(newPermission) => setCurrentPermission(newPermission)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BulkEditModal;
|
Loading…
Reference in new issue