parent
ddaca7b0c4
commit
20c8dde197
|
|
@ -1,4 +1,8 @@
|
||||||
import { HelpCircle } from "@styled-icons/boxicons-solid";
|
import {
|
||||||
|
HelpCircle,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
} from "@styled-icons/boxicons-solid";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Server } from "revolt.js";
|
import { Server } from "revolt.js";
|
||||||
|
|
@ -21,12 +25,191 @@ import {
|
||||||
import Tooltip from "../../../components/common/Tooltip";
|
import Tooltip from "../../../components/common/Tooltip";
|
||||||
import { PermissionList } from "../../../components/settings/roles/PermissionList";
|
import { PermissionList } from "../../../components/settings/roles/PermissionList";
|
||||||
import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection";
|
import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection";
|
||||||
|
import { useSession } from "../../../controllers/client/ClientController";
|
||||||
import { modalController } from "../../../controllers/modals/ModalController";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
server: Server;
|
server: Server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RoleReorderContainer = styled.div`
|
||||||
|
margin: 16px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleItem = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 12px 0;
|
||||||
|
background: var(--secondary-background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleInfo = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleName = styled.div`
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleRank = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleControls = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MoveButton = styled(Button)`
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-width: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to memo-ize role information with proper ordering
|
||||||
|
* @param server Target server
|
||||||
|
* @returns Role array with default at bottom
|
||||||
|
*/
|
||||||
|
export function useRolesForReorder(server: Server) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const roles = [...server.orderedRoles] as RoleOrDefault[];
|
||||||
|
|
||||||
|
roles.push({
|
||||||
|
id: "default",
|
||||||
|
name: "Default",
|
||||||
|
permissions: server.default_permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}, [server.roles, server.default_permissions]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role reordering component
|
||||||
|
*/
|
||||||
|
const RoleReorderPanel = observer(({ server }: Props) => {
|
||||||
|
const initialRoles = useRolesForReorder(server);
|
||||||
|
const [roles, setRoles] = useState(initialRoles);
|
||||||
|
const [isReordering, setIsReordering] = useState(false);
|
||||||
|
|
||||||
|
// Update local state when server roles change
|
||||||
|
useMemo(() => {
|
||||||
|
setRoles(useRolesForReorder(server));
|
||||||
|
}, [server.roles, server.default_permissions]);
|
||||||
|
|
||||||
|
const moveRoleUp = (index: number) => {
|
||||||
|
if (index === 0 || roles[index].id === "default") return;
|
||||||
|
|
||||||
|
const newRoles = [...roles];
|
||||||
|
[newRoles[index - 1], newRoles[index]] = [
|
||||||
|
newRoles[index],
|
||||||
|
newRoles[index - 1],
|
||||||
|
];
|
||||||
|
setRoles(newRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveRoleDown = (index: number) => {
|
||||||
|
// Can't move down if it's the last non-default role or if it's default
|
||||||
|
if (index >= roles.length - 2 || roles[index].id === "default") return;
|
||||||
|
|
||||||
|
const newRoles = [...roles];
|
||||||
|
[newRoles[index], newRoles[index + 1]] = [
|
||||||
|
newRoles[index + 1],
|
||||||
|
newRoles[index],
|
||||||
|
];
|
||||||
|
setRoles(newRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveReorder = async () => {
|
||||||
|
setIsReordering(true);
|
||||||
|
try {
|
||||||
|
const nonDefaultRoles = roles.filter(
|
||||||
|
(role) => role.id !== "default",
|
||||||
|
);
|
||||||
|
const roleIds = nonDefaultRoles.map((role) => role.id);
|
||||||
|
|
||||||
|
const session = useSession()!;
|
||||||
|
const client = session.client!;
|
||||||
|
|
||||||
|
// Make direct API request since it's not in r.js as of writing
|
||||||
|
await client.api.patch(`/servers/${server._id}/roles/ranks`, {
|
||||||
|
ranks: roleIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Roles reordered successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reorder roles:", error);
|
||||||
|
setRoles(initialRoles);
|
||||||
|
} finally {
|
||||||
|
setIsReordering(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasChanges = !isEqual(
|
||||||
|
roles.filter((r) => r.id !== "default").map((r) => r.id),
|
||||||
|
initialRoles.filter((r) => r.id !== "default").map((r) => r.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SpaceBetween>
|
||||||
|
<H1>
|
||||||
|
<Text id="app.settings.permissions.role_ranking" />
|
||||||
|
</H1>
|
||||||
|
<Button
|
||||||
|
palette="secondary"
|
||||||
|
disabled={!hasChanges || isReordering}
|
||||||
|
onClick={saveReorder}>
|
||||||
|
<Text id="app.special.modals.actions.save" />
|
||||||
|
</Button>
|
||||||
|
</SpaceBetween>
|
||||||
|
|
||||||
|
<RoleReorderContainer>
|
||||||
|
{roles.map((role, index) => (
|
||||||
|
<RoleItem key={role.id}>
|
||||||
|
<RoleInfo>
|
||||||
|
<RoleName>{role.name}</RoleName>
|
||||||
|
<RoleRank>
|
||||||
|
{role.id === "default" ? (
|
||||||
|
<Text id="app.settings.permissions.default_desc" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text id="app.settings.permissions.role_ranking" />{" "}
|
||||||
|
{index}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</RoleRank>
|
||||||
|
</RoleInfo>
|
||||||
|
|
||||||
|
{role.id !== "default" && (
|
||||||
|
<RoleControls>
|
||||||
|
<MoveButton
|
||||||
|
palette="secondary"
|
||||||
|
disabled={index === 0}
|
||||||
|
onClick={() => moveRoleUp(index)}>
|
||||||
|
<ChevronUp size={16} />
|
||||||
|
</MoveButton>
|
||||||
|
<MoveButton
|
||||||
|
palette="secondary"
|
||||||
|
disabled={index >= roles.length - 2}
|
||||||
|
onClick={() => moveRoleDown(index)}>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</MoveButton>
|
||||||
|
</RoleControls>
|
||||||
|
)}
|
||||||
|
</RoleItem>
|
||||||
|
))}
|
||||||
|
</RoleReorderContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to memo-ize role information.
|
* Hook to memo-ize role information.
|
||||||
* @param server Target server
|
* @param server Target server
|
||||||
|
|
@ -50,9 +233,11 @@ export function useRoles(server: Server) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roles settings menu
|
* Updated Roles settings menu with reordering panel
|
||||||
*/
|
*/
|
||||||
export const Roles = observer(({ server }: Props) => {
|
export const Roles = observer(({ server }: Props) => {
|
||||||
|
const [showReorderPanel, setShowReorderPanel] = useState(false);
|
||||||
|
|
||||||
// Consolidate all permissions that we can change right now.
|
// Consolidate all permissions that we can change right now.
|
||||||
const currentRoles = useRoles(server);
|
const currentRoles = useRoles(server);
|
||||||
|
|
||||||
|
|
@ -74,193 +259,222 @@ export const Roles = observer(({ server }: Props) => {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ReorderButton = styled(Button)`
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (showReorderPanel) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<RoleReorderPanel server={server} />
|
||||||
|
<Button
|
||||||
|
palette="secondary"
|
||||||
|
onClick={() => setShowReorderPanel(false)}
|
||||||
|
style={{ marginBottom: "16px" }}>
|
||||||
|
<Text id="app.special.modals.actions.back" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PermissionsLayout
|
<div>
|
||||||
server={server}
|
<ReorderButton
|
||||||
rank={server.member?.ranking ?? Infinity}
|
palette="secondary"
|
||||||
onCreateRole={(callback) =>
|
onClick={() => setShowReorderPanel(true)}>
|
||||||
modalController.push({
|
<Text id="app.settings.permissions.role_ranking" />
|
||||||
type: "create_role",
|
</ReorderButton>
|
||||||
server,
|
<PermissionsLayout
|
||||||
callback,
|
server={server}
|
||||||
})
|
rank={server.member?.ranking ?? Infinity}
|
||||||
}
|
onCreateRole={(callback) =>
|
||||||
editor={({ selected }) => {
|
modalController.push({
|
||||||
const currentRole = currentRoles.find(
|
type: "create_role",
|
||||||
(x) => x.id === selected,
|
server,
|
||||||
)!;
|
callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editor={({ selected }) => {
|
||||||
|
const currentRole = currentRoles.find(
|
||||||
|
(x) => x.id === selected,
|
||||||
|
)!;
|
||||||
|
|
||||||
if (!currentRole) return null;
|
if (!currentRole) return null;
|
||||||
|
|
||||||
// Keep track of whatever role we're editing right now.
|
const [value, setValue] = useState<Partial<RoleOrDefault>>(
|
||||||
const [value, setValue] = useState<Partial<RoleOrDefault>>({});
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
const currentRoleValue = { ...currentRole, ...value };
|
const currentRoleValue = { ...currentRole, ...value };
|
||||||
|
|
||||||
// Calculate permissions we have access to on this server.
|
function save() {
|
||||||
const current = server.permission;
|
const { permissions: permsCurrent, ...current } =
|
||||||
|
currentRole;
|
||||||
|
const { permissions: permsValue, ...value } =
|
||||||
|
currentRoleValue;
|
||||||
|
|
||||||
// Upload new role information to server.
|
if (!isEqual(permsCurrent, permsValue)) {
|
||||||
function save() {
|
server.setPermissions(
|
||||||
const { permissions: permsCurrent, ...current } =
|
selected,
|
||||||
currentRole;
|
typeof permsValue === "number"
|
||||||
const { permissions: permsValue, ...value } =
|
? permsValue
|
||||||
currentRoleValue;
|
: {
|
||||||
|
allow: permsValue.a,
|
||||||
|
deny: permsValue.d,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEqual(permsCurrent, permsValue)) {
|
if (!isEqual(current, value)) {
|
||||||
server.setPermissions(
|
server.editRole(selected, value);
|
||||||
selected,
|
}
|
||||||
typeof permsValue === "number"
|
|
||||||
? permsValue
|
|
||||||
: {
|
|
||||||
allow: permsValue.a,
|
|
||||||
deny: permsValue.d,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEqual(current, value)) {
|
function deleteRole() {
|
||||||
server.editRole(selected, value);
|
server.deleteRole(selected);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the role from this server.
|
return (
|
||||||
function deleteRole() {
|
<div>
|
||||||
server.deleteRole(selected);
|
<SpaceBetween>
|
||||||
}
|
<H1>
|
||||||
|
<Text
|
||||||
return (
|
id="app.settings.actions.edit"
|
||||||
<div>
|
fields={{ name: currentRole.name }}
|
||||||
<SpaceBetween>
|
/>
|
||||||
<H1>
|
</H1>
|
||||||
<Text
|
<Button
|
||||||
id="app.settings.actions.edit"
|
palette="secondary"
|
||||||
fields={{ name: currentRole.name }}
|
disabled={isEqual(
|
||||||
/>
|
currentRole,
|
||||||
</H1>
|
currentRoleValue,
|
||||||
<Button
|
)}
|
||||||
palette="secondary"
|
onClick={save}>
|
||||||
disabled={isEqual(
|
<Text id="app.special.modals.actions.save" />
|
||||||
currentRole,
|
</Button>
|
||||||
currentRoleValue,
|
</SpaceBetween>
|
||||||
)}
|
<hr />
|
||||||
onClick={save}>
|
{selected !== "default" && (
|
||||||
<Text id="app.special.modals.actions.save" />
|
<>
|
||||||
</Button>
|
<section>
|
||||||
</SpaceBetween>
|
<Category>
|
||||||
<hr />
|
<Text id="app.settings.permissions.role_name" />
|
||||||
{selected !== "default" && (
|
</Category>
|
||||||
<>
|
<p>
|
||||||
<section>
|
<InputBox
|
||||||
<Category>
|
value={currentRoleValue.name}
|
||||||
<Text id="app.settings.permissions.role_name" />
|
onChange={(e) =>
|
||||||
</Category>
|
setValue({
|
||||||
<p>
|
...value,
|
||||||
<InputBox
|
name: e.currentTarget
|
||||||
value={currentRoleValue.name}
|
.value,
|
||||||
onChange={(e) =>
|
})
|
||||||
setValue({
|
}
|
||||||
...value,
|
palette="secondary"
|
||||||
name: e.currentTarget.value,
|
/>
|
||||||
})
|
</p>
|
||||||
}
|
</section>
|
||||||
palette="secondary"
|
<section>
|
||||||
/>
|
<Category>{"Role ID"}</Category>
|
||||||
</p>
|
<RoleId>
|
||||||
</section>
|
<Tooltip
|
||||||
<section>
|
content={
|
||||||
<Category>{"Role ID"}</Category>
|
"This is a unique identifier for this role."
|
||||||
<RoleId>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
"This is a unique identifier for this role."
|
|
||||||
}>
|
|
||||||
<HelpCircle size={16} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
<Text id="app.special.copy" />
|
|
||||||
}>
|
|
||||||
<a
|
|
||||||
onClick={() =>
|
|
||||||
modalController.writeText(
|
|
||||||
currentRole.id,
|
|
||||||
)
|
|
||||||
}>
|
}>
|
||||||
{currentRole.id}
|
<HelpCircle size={16} />
|
||||||
</a>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
</RoleId>
|
content={
|
||||||
</section>
|
<Text id="app.special.copy" />
|
||||||
<section>
|
}>
|
||||||
<Category>
|
<a
|
||||||
<Text id="app.settings.permissions.role_colour" />
|
onClick={() =>
|
||||||
</Category>
|
modalController.writeText(
|
||||||
<p>
|
currentRole.id,
|
||||||
<ColourSwatches
|
)
|
||||||
value={
|
}>
|
||||||
currentRoleValue.colour ??
|
{currentRole.id}
|
||||||
"gray"
|
</a>
|
||||||
}
|
</Tooltip>
|
||||||
onChange={(colour) =>
|
</RoleId>
|
||||||
setValue({ ...value, colour })
|
</section>
|
||||||
}
|
<section>
|
||||||
/>
|
<Category>
|
||||||
</p>
|
<Text id="app.settings.permissions.role_colour" />
|
||||||
</section>
|
</Category>
|
||||||
<section>
|
<p>
|
||||||
<Category>
|
<ColourSwatches
|
||||||
<Text id="app.settings.permissions.role_options" />
|
value={
|
||||||
</Category>
|
currentRoleValue.colour ??
|
||||||
<p>
|
"gray"
|
||||||
<Checkbox
|
}
|
||||||
value={
|
onChange={(colour) =>
|
||||||
currentRoleValue.hoist ?? false
|
setValue({
|
||||||
}
|
...value,
|
||||||
onChange={(hoist) =>
|
colour,
|
||||||
setValue({ ...value, hoist })
|
})
|
||||||
}
|
}
|
||||||
title={
|
/>
|
||||||
<Text id="app.settings.permissions.hoist_role" />
|
</p>
|
||||||
}
|
</section>
|
||||||
description={
|
<section>
|
||||||
<Text id="app.settings.permissions.hoist_desc" />
|
<Category>
|
||||||
}
|
<Text id="app.settings.permissions.role_options" />
|
||||||
/>
|
</Category>
|
||||||
</p>
|
<p>
|
||||||
</section>
|
<Checkbox
|
||||||
</>
|
value={
|
||||||
)}
|
currentRoleValue.hoist ??
|
||||||
<h1>
|
false
|
||||||
<Text id="app.settings.permissions.edit_title" />
|
}
|
||||||
</h1>
|
onChange={(hoist) =>
|
||||||
<PermissionList
|
setValue({
|
||||||
value={currentRoleValue.permissions}
|
...value,
|
||||||
onChange={(permissions) =>
|
hoist,
|
||||||
setValue({
|
})
|
||||||
...value,
|
}
|
||||||
permissions,
|
title={
|
||||||
} as RoleOrDefault)
|
<Text id="app.settings.permissions.hoist_role" />
|
||||||
}
|
}
|
||||||
target={server}
|
description={
|
||||||
/>
|
<Text id="app.settings.permissions.hoist_desc" />
|
||||||
{selected !== "default" && (
|
}
|
||||||
<>
|
/>
|
||||||
<hr />
|
</p>
|
||||||
<h1>
|
</section>
|
||||||
<Text id="app.settings.categories.danger_zone" />
|
</>
|
||||||
</h1>
|
)}
|
||||||
<DeleteRoleButton
|
<h1>
|
||||||
palette="error"
|
<Text id="app.settings.permissions.edit_title" />
|
||||||
compact
|
</h1>
|
||||||
onClick={deleteRole}>
|
<PermissionList
|
||||||
<Text id="app.settings.permissions.delete_role" />
|
value={currentRoleValue.permissions}
|
||||||
</DeleteRoleButton>
|
onChange={(permissions) =>
|
||||||
</>
|
setValue({
|
||||||
)}
|
...value,
|
||||||
</div>
|
permissions,
|
||||||
);
|
} as RoleOrDefault)
|
||||||
}}
|
}
|
||||||
/>
|
target={server}
|
||||||
|
/>
|
||||||
|
{selected !== "default" && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<h1>
|
||||||
|
<Text id="app.settings.categories.danger_zone" />
|
||||||
|
</h1>
|
||||||
|
<DeleteRoleButton
|
||||||
|
palette="error"
|
||||||
|
compact
|
||||||
|
onClick={deleteRole}>
|
||||||
|
<Text id="app.settings.permissions.delete_role" />
|
||||||
|
</DeleteRoleButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue