diff --git a/src/pages/settings/server/Roles.tsx b/src/pages/settings/server/Roles.tsx index 7527bb51..927e9bb8 100644 --- a/src/pages/settings/server/Roles.tsx +++ b/src/pages/settings/server/Roles.tsx @@ -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 { observer } from "mobx-react-lite"; import { Server } from "revolt.js"; @@ -21,12 +25,198 @@ import { import Tooltip from "../../../components/common/Tooltip"; import { PermissionList } from "../../../components/settings/roles/PermissionList"; import { RoleOrDefault } from "../../../components/settings/roles/RoleSelection"; +import { useSession } from "../../../controllers/client/ClientController"; import { modalController } from "../../../controllers/modals/ModalController"; interface Props { 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 ( +
+ +

+ +

+ +
+ + + {roles.map((role, index) => ( + + + {role.name} + + {role.id === "default" ? ( + + ) : ( + <> + {" "} + {index} + + )} + + + + {role.id !== "default" && ( + + moveRoleUp(index)}> + + + = roles.length - 2} + onClick={() => moveRoleDown(index)}> + + + + )} + + ))} + +
+ ); + }, +); + /** * Hook to memo-ize role information. * @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) => { + const [showReorderPanel, setShowReorderPanel] = useState(false); + const [rolesWereReordered, setRolesWereReordered] = useState(false); + // Consolidate all permissions that we can change right now. const currentRoles = useRoles(server); @@ -74,213 +267,232 @@ export const Roles = observer(({ server }: Props) => { 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 ( +
+ setRolesWereReordered(true)} + /> + +
+ ); + } + return ( - - modalController.push({ - type: "create_role", - server, - callback, - }) - } - editor={({ selected }) => { - const currentRole = currentRoles.find( - (x) => x.id === selected, - )!; +
+ + modalController.push({ + type: "create_role", + 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>({}); + const [value, setValue] = useState>( + {}, + ); - const currentRoleValue = { ...currentRole, ...value }; + const currentRoleValue = { ...currentRole, ...value }; - // Calculate permissions we have access to on this server. - const current = server.permission; + function save() { + const { permissions: permsCurrent, ...current } = + currentRole; + const { permissions: permsValue, ...value } = + currentRoleValue; - // Upload new role information to server. - function save() { - const { permissions: permsCurrent, ...current } = - currentRole; - const { permissions: permsValue, ...value } = - currentRoleValue; + if (!isEqual(permsCurrent, permsValue)) { + server.setPermissions( + selected, + typeof permsValue === "number" + ? permsValue + : { + allow: permsValue.a, + deny: permsValue.d, + }, + ); + } - if (!isEqual(permsCurrent, permsValue)) { - server.setPermissions( - selected, - typeof permsValue === "number" - ? permsValue - : { - allow: permsValue.a, - deny: permsValue.d, - }, - ); + if (!isEqual(current, value)) { + server.editRole(selected, value); + } } - if (!isEqual(current, value)) { - server.editRole(selected, value); + function deleteRole() { + server.deleteRole(selected); } - } - // Delete the role from this server. - function deleteRole() { - server.deleteRole(selected); - } - - return ( -
- -

- -

- -
-
- {selected !== "default" && ( - <> -
- - - -

- - setValue({ - ...value, - name: e.currentTarget.value, - }) - } - palette="secondary" - /> -

-
-
- {"Role ID"} - - - - - - }> - - modalController.writeText( - currentRole.id, - ) + return ( +
+ +

+ +

+ setShowReorderPanel(true)}> + + + +
+
+ {selected !== "default" && ( + <> +
+ + + +

+ + setValue({ + ...value, + name: e.currentTarget + .value, + }) + } + palette="secondary" + /> +

+
+
+ {"Role ID"} + + - {currentRole.id} - - - -
-
- - - -

- - setValue({ ...value, colour }) - } - /> -

-
-
- - - -

- - setValue({ ...value, hoist }) - } - title={ - - } - description={ - - } - /> -

-
-
- - - -

- - setValue({ - ...value, - rank: parseInt( - e.currentTarget.value, - ), - }) - } - palette="secondary" - /> -

-
- - )} -

- -

- - setValue({ - ...value, - permissions, - } as RoleOrDefault) - } - target={server} - /> - {selected !== "default" && ( - <> -
-

- -

- - - - - )} -
- ); - }} - /> + +
+ + }> + + modalController.writeText( + currentRole.id, + ) + }> + {currentRole.id} + + +
+
+
+ + + +

+ + setValue({ + ...value, + colour, + }) + } + /> +

+
+
+ + + +

+ + setValue({ + ...value, + hoist, + }) + } + title={ + + } + description={ + + } + /> +

+
+ + )} +

+ +

+ + setValue({ + ...value, + permissions, + } as RoleOrDefault) + } + target={server} + /> + {selected !== "default" && ( + <> +
+

+ +

+ + + + + )} +
+ ); + }} + /> +
); });