feat(permission): implement new server / channel permission menus

This commit is contained in:
Paul Makles
2022-02-27 23:44:29 +00:00
parent 17ae96fb87
commit 48fb8c847f
17 changed files with 587 additions and 311 deletions

View File

@@ -0,0 +1,79 @@
import { Check, Square, X } from "@styled-icons/boxicons-regular";
import styled, { css } from "styled-components";
type State = "Allow" | "Neutral" | "Deny";
const SwitchContainer = styled.div.attrs({
role: "radiogroup",
"aria-orientiation": "horizontal",
})`
flex-shrink: 0;
display: flex;
margin: 4px 16px;
overflow: hidden;
border-radius: var(--border-radius);
background: var(--secondary-background);
border: 2px solid var(--tertiary-background);
`;
const Switch = styled.div.attrs({
role: "radio",
})<{ state: State; selected: boolean }>`
padding: 4px;
cursor: pointer;
transition: 0.2s ease all;
color: ${(props) =>
props.state === "Allow"
? "var(--success)"
: props.state === "Deny"
? "var(--error)"
: "var(--tertiary-foreground)"};
${(props) =>
props.selected &&
css`
color: white;
background: ${props.state === "Allow"
? "var(--success)"
: props.state === "Deny"
? "var(--error)"
: "var(--primary-background)"};
`}
&:hover {
filter: brightness(0.8);
}
`;
interface Props {
state: State;
onChange: (state: State) => void;
}
export function OverrideSwitch({ state, onChange }: Props) {
return (
<SwitchContainer>
<Switch
onClick={() => onChange("Deny")}
state="Deny"
selected={state === "Deny"}>
<X size={24} />
</Switch>
<Switch
onClick={() => onChange("Neutral")}
state="Neutral"
selected={state === "Neutral"}>
<Square size={24} />
</Switch>
<Switch
onClick={() => onChange("Allow")}
state="Allow"
selected={state === "Allow"}>
<Check size={24} />
</Switch>
</SwitchContainer>
);
}

View File

@@ -0,0 +1,33 @@
import { OverrideField } from "revolt-api/types/_common";
import { Permission } from "revolt.js";
import { PermissionSelect } from "./PermissionSelect";
interface Props {
value: OverrideField | number;
onChange: (v: OverrideField | number) => void;
filter?: (keyof typeof Permission)[];
}
export function PermissionList({ value, onChange, filter }: Props) {
return (
<>
{(Object.keys(Permission) as (keyof typeof Permission)[])
.filter(
(key) =>
key !== "GrantAllSafe" &&
(!filter || filter.includes(key)),
)
.map((x) => (
<PermissionSelect
id={x}
key={x}
permission={Permission[x]}
value={value}
onChange={onChange}
/>
))}
</>
);
}

View File

@@ -0,0 +1,124 @@
import Long from "long";
import { OverrideField } from "revolt-api/types/_common";
import { Permission } from "revolt.js";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { useMemo } from "preact/hooks";
import Checkbox from "../../ui/Checkbox";
import { OverrideSwitch } from "./OverrideSwitch";
interface PermissionSelectProps {
id: keyof typeof Permission;
permission: number;
value: OverrideField | number;
onChange: (value: OverrideField | number) => void;
}
type State = "Allow" | "Neutral" | "Deny";
const PermissionEntry = styled.label`
width: 100%;
margin: 8px 0;
display: flex;
font-size: 1.1em;
align-items: center;
.title {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.description {
font-size: 0.8em;
color: var(--secondary-foreground);
}
`;
export function PermissionSelect({
id,
permission,
value,
onChange,
}: PermissionSelectProps) {
const state: State = useMemo(() => {
if (typeof value === "object") {
if (Long.fromNumber(value.d).and(permission).eq(permission)) {
return "Deny";
}
if (Long.fromNumber(value.a).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
} else {
if (Long.fromNumber(value).and(permission).eq(permission)) {
return "Allow";
}
return "Neutral";
}
}, [value]);
function onSwitch(state: State) {
if (typeof value !== "object") throw "!";
// Convert to Long so we can do bitwise ops.
let allow = Long.fromNumber(value.a);
let deny = Long.fromNumber(value.d);
// Clear the current permission value.
if (allow.and(permission).eq(permission)) {
allow = allow.xor(permission);
}
if (deny.and(permission).eq(permission)) {
deny = deny.xor(permission);
}
// Apply the current permission state.
if (state === "Allow") {
allow = allow.or(permission);
}
if (state === "Deny") {
deny = deny.or(permission);
}
// Invoke state change.
onChange({
a: allow.toNumber(),
d: deny.toNumber(),
});
}
return (
<PermissionEntry>
<span class="title">
<Text id={`permissions.server.${id}.t`}>{id}</Text>
<span class="description">
<Text id={`permissions.server.${id}.d`} />
</span>
</span>
{typeof value === "object" ? (
<OverrideSwitch state={state} onChange={onSwitch} />
) : (
<Checkbox
checked={state === "Allow"}
onChange={() =>
onChange(
Long.fromNumber(value, false)
.xor(permission)
.toNumber(),
)
}
/>
)}
</PermissionEntry>
);
}

View File

@@ -0,0 +1,35 @@
import { Role } from "revolt-api/types/Servers";
import Checkbox from "../../ui/Checkbox";
export type RoleOrDefault = (
| Role
| {
name: string;
permissions: number;
colour?: string;
hoist?: boolean;
rank?: number;
}
) & { id: string };
interface Props {
selected: string;
onSelect: (id: string) => void;
roles: RoleOrDefault[];
}
export function RoleSelection({ selected, onSelect, roles }: Props) {
return (
<>
{roles.map((x) => (
<Checkbox
checked={x.id === selected}
onChange={() => onSelect(x.id)}>
{x.name}
</Checkbox>
))}
</>
);
}

View File

@@ -0,0 +1,22 @@
import Tip from "../../../components/ui/Tip";
import Button from "../../ui/Button";
interface Props {
save: () => void;
}
export function UnsavedChanges({ save }: Props) {
return (
<Tip hideSeparator>
<span
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}>
You have unsaved changes!
<Button onClick={save}>Save</Button>
</span>
</Tip>
);
}