merge: remote-tracking branch 'origin/role-reordering'

pull/1139/head
izzy 2025-08-07 12:03:35 +02:00
commit 8ba7a769b3
1 changed files with 410 additions and 198 deletions

View File

@ -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,198 @@ 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,
onRolesReordered,
}: Props & { onRolesReordered: () => void }) => {
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");
onRolesReordered();
} 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 +240,12 @@ 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);
const [rolesWereReordered, setRolesWereReordered] = 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,213 +267,232 @@ export const Roles = observer(({ server }: Props) => {
margin: 16px 0; margin: 16px 0;
`; `;
const ReorderButton = styled(Button)`
margin-inline: auto 8px;
`;
const handleBackFromReorder = () => {
setShowReorderPanel(false);
if (rolesWereReordered) {
window.location.reload(); // Refresh because I don't actually care anymore.
}
};
if (showReorderPanel) {
return (
<div>
<RoleReorderPanel
server={server}
onRolesReordered={() => setRolesWereReordered(true)}
/>
<Button
palette="secondary"
onClick={handleBackFromReorder}
style={{ marginBottom: "16px" }}>
<Text id="app.special.modals.actions.back" />
</Button>
</div>
);
}
return ( return (
<PermissionsLayout <div>
server={server} <PermissionsLayout
rank={server.member?.ranking ?? Infinity} server={server}
onCreateRole={(callback) => rank={server.member?.ranking ?? Infinity}
modalController.push({ onCreateRole={(callback) =>
type: "create_role", modalController.push({
server, type: "create_role",
callback, server,
}) callback,
} })
editor={({ selected }) => { }
const currentRole = currentRoles.find( editor={({ selected }) => {
(x) => x.id === 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 <ReorderButton
id="app.settings.actions.edit" palette="secondary"
fields={{ name: currentRole.name }} onClick={() => setShowReorderPanel(true)}>
/> <Text id="app.settings.permissions.role_ranking" />
</H1> </ReorderButton>
<Button <Button
palette="secondary" palette="secondary"
disabled={isEqual( disabled={isEqual(
currentRole, currentRole,
currentRoleValue, currentRoleValue,
)} )}
onClick={save}> onClick={save}>
<Text id="app.special.modals.actions.save" /> <Text id="app.special.modals.actions.save" />
</Button> </Button>
</SpaceBetween> </SpaceBetween>
<hr /> <hr />
{selected !== "default" && ( {selected !== "default" && (
<> <>
<section> <section>
<Category> <Category>
<Text id="app.settings.permissions.role_name" /> <Text id="app.settings.permissions.role_name" />
</Category> </Category>
<p> <p>
<InputBox <InputBox
value={currentRoleValue.name} value={currentRoleValue.name}
onChange={(e) => onChange={(e) =>
setValue({ setValue({
...value, ...value,
name: e.currentTarget.value, name: e.currentTarget
}) .value,
} })
palette="secondary" }
/> palette="secondary"
</p> />
</section> </p>
<section> </section>
<Category>{"Role ID"}</Category> <section>
<RoleId> <Category>{"Role ID"}</Category>
<Tooltip <RoleId>
content={ <Tooltip
"This is a unique identifier for this role." 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
<section> value={
<Category> currentRoleValue.hoist ??
<Text id="app.settings.permissions.role_ranking" /> false
</Category> }
<p> onChange={(hoist) =>
<InputBox setValue({
type="number" ...value,
value={currentRoleValue.rank ?? 0} hoist,
onChange={(e) => })
setValue({ }
...value, title={
rank: parseInt( <Text id="app.settings.permissions.hoist_role" />
e.currentTarget.value, }
), description={
}) <Text id="app.settings.permissions.hoist_desc" />
} }
palette="secondary" />
/> </p>
</p> </section>
</section> </>
</> )}
)} <h1>
<h1> <Text id="app.settings.permissions.edit_title" />
<Text id="app.settings.permissions.edit_title" /> </h1>
</h1> <PermissionList
<PermissionList value={currentRoleValue.permissions}
value={currentRoleValue.permissions} onChange={(permissions) =>
onChange={(permissions) => setValue({
setValue({ ...value,
...value, permissions,
permissions, } as RoleOrDefault)
} as RoleOrDefault) }
} target={server}
target={server} />
/> {selected !== "default" && (
{selected !== "default" && ( <>
<> <hr />
<hr /> <h1>
<h1> <Text id="app.settings.categories.danger_zone" />
<Text id="app.settings.categories.danger_zone" /> </h1>
</h1> <DeleteRoleButton
<DeleteRoleButton palette="error"
palette="error" compact
compact onClick={deleteRole}>
onClick={deleteRole}> <Text id="app.settings.permissions.delete_role" />
<Text id="app.settings.permissions.delete_role" /> </DeleteRoleButton>
</DeleteRoleButton> </>
</> )}
)} </div>
</div> );
); }}
}} />
/> </div>
); );
}); });