feat: Add role re-ordering

Doesn't currently update order in UI
master
Declan Chidlow 2025-06-12 12:33:39 +08:00
parent ddaca7b0c4
commit 20c8dde197
1 changed files with 392 additions and 178 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,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>
); );
}); });