mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-15 00:59:09 +00:00
feat: add access control
This commit is contained in:
parent
9c07ff0e54
commit
3f45153b66
47 changed files with 1084 additions and 164 deletions
|
@ -20,7 +20,7 @@ export const Container = styled.div<ContainerProps>`
|
|||
text-align: center;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
font-size: ${(props: ContainerProps) => props.size};
|
||||
font-size: ${(props: ContainerProps) => (props.size || 48) * 0.45}px;
|
||||
${(props) =>
|
||||
props.deliberatelyEmpty &&
|
||||
css`
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { IconHelper } from './IconHelper';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Icon Helper',
|
||||
component: IconHelper,
|
||||
};
|
||||
|
||||
export const iconHelper = (args) => <IconHelper {...args} />;
|
|
@ -0,0 +1,21 @@
|
|||
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const levelColors = {
|
||||
error: palette.red400,
|
||||
warn: palette.gold400,
|
||||
info: palette.discord400,
|
||||
chrome: palette.taupe400,
|
||||
success: palette.green400,
|
||||
none: 'unset',
|
||||
};
|
||||
|
||||
export type IconHelperLevel = keyof typeof levelColors;
|
||||
|
||||
export const IconHelperStyled = styled.span<{
|
||||
level: IconHelperLevel;
|
||||
}>`
|
||||
position: relative;
|
||||
top: 0.12em;
|
||||
color: ${(props) => levelColors[props.level]};
|
||||
`;
|
7
packages/design-system/atoms/icon-helper/IconHelper.tsx
Normal file
7
packages/design-system/atoms/icon-helper/IconHelper.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { IconHelperLevel, IconHelperStyled } from './IconHelper.styled';
|
||||
|
||||
export const IconHelper = (props: {
|
||||
children: React.ReactNode;
|
||||
level?: IconHelperLevel;
|
||||
}) => <IconHelperStyled level={props.level || 'none'}>{props.children}</IconHelperStyled>;
|
1
packages/design-system/atoms/icon-helper/index.ts
Normal file
1
packages/design-system/atoms/icon-helper/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './IconHelper';
|
|
@ -82,6 +82,10 @@ export const Role = (props: Props) => {
|
|||
|
||||
const disabledReason = (role: RPCRole) => {
|
||||
switch (role.safety) {
|
||||
case RoleSafety.ManagedRole:
|
||||
return 'This role is managed by an integration/bot.';
|
||||
case RoleSafety.AccessControl:
|
||||
return 'This role is part of the allow/block list for the server.';
|
||||
case RoleSafety.HigherThanBot:
|
||||
return `This role is above Roleypoly's own role.`;
|
||||
case RoleSafety.DangerousPermissions:
|
||||
|
|
|
@ -134,9 +134,9 @@ export const roleWikiData = {
|
|||
};
|
||||
|
||||
export const guild: Guild = {
|
||||
name: 'emoji megaporium',
|
||||
id: '421896162539470888',
|
||||
icon: '3372fd895ed913b55616c5e49cd50e60',
|
||||
name: 'Roleypoly',
|
||||
id: '386659935687147521',
|
||||
icon: 'ffee638c73ff9c972554f64ca34d67ee',
|
||||
roles: [],
|
||||
};
|
||||
|
||||
|
@ -169,12 +169,37 @@ export const guildMap: { [x: string]: GuildSlug } = {
|
|||
},
|
||||
};
|
||||
|
||||
const blockedRole: Role = {
|
||||
id: 'blocked',
|
||||
permissions: '0',
|
||||
name: 'blocked',
|
||||
color: 0xff0000,
|
||||
position: 0,
|
||||
managed: false,
|
||||
safety: RoleSafety.Safe,
|
||||
};
|
||||
|
||||
const allowedRole: Role = {
|
||||
id: 'allowed',
|
||||
permissions: '0',
|
||||
name: 'allowed',
|
||||
color: 0x00ff00,
|
||||
position: 0,
|
||||
managed: false,
|
||||
safety: RoleSafety.Safe,
|
||||
};
|
||||
|
||||
export const guildData: GuildData = {
|
||||
id: 'aaa',
|
||||
id: '386659935687147521',
|
||||
message: 'henlo worl!!',
|
||||
categories: [mockCategory, mockCategorySingle],
|
||||
features: Features.None,
|
||||
auditLogWebhook: null,
|
||||
accessControl: {
|
||||
blockList: [blockedRole.id],
|
||||
allowList: [allowedRole.id],
|
||||
blockPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const user: DiscordUser = {
|
||||
|
@ -206,14 +231,14 @@ export const guildEnum: GuildEnumeration = {
|
|||
roles: [...roleCategory, ...roleCategory2],
|
||||
},
|
||||
{
|
||||
id: 'bbb',
|
||||
id: '386659935687147521',
|
||||
guild: guildMap['Roleypoly'],
|
||||
member: {
|
||||
...member,
|
||||
roles: ['unsafe2'],
|
||||
},
|
||||
data: guildData,
|
||||
roles: [...roleCategory, ...roleCategory2],
|
||||
roles: [...roleCategory, ...roleCategory2, blockedRole, allowedRole],
|
||||
},
|
||||
{
|
||||
id: 'ccc',
|
||||
|
@ -232,6 +257,8 @@ export const guildEnum: GuildEnumeration = {
|
|||
],
|
||||
};
|
||||
|
||||
export const presentableGuild = guildEnum.guilds[1];
|
||||
|
||||
export const mastheadSlugs: GuildSlug[] = guildEnum.guilds.map<GuildSlug>(
|
||||
(guild, idx) => ({
|
||||
id: guild.guild.id,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { presentableGuild } from '../../fixtures/storyData';
|
||||
import { EditableRoleList } from './EditableRoleList';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Editable Role List',
|
||||
component: EditableRoleList,
|
||||
args: {
|
||||
roles: presentableGuild.roles,
|
||||
selectedRoles: presentableGuild.data.categories[0].roles,
|
||||
unselectedRoles: presentableGuild.roles.filter(
|
||||
(r) => !presentableGuild.data.categories[0].roles.includes(r.id)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const editableRoleList = (args) => <EditableRoleList {...args} />;
|
|
@ -0,0 +1,43 @@
|
|||
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||
import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const EditableRoleListStyled = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
& > div {
|
||||
margin: 2.5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AddRoleButton = styled.div<{ long?: boolean }>`
|
||||
border: 2px solid ${palette.taupe500};
|
||||
color: ${palette.taupe500};
|
||||
border-radius: 24px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all ${transitions.actionable}s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: ${palette.taupe100};
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.long
|
||||
? css`
|
||||
padding: 0 14px;
|
||||
`
|
||||
: css`
|
||||
width: 32px;
|
||||
`};
|
||||
`;
|
|
@ -0,0 +1,103 @@
|
|||
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
||||
import { Role } from '@roleypoly/design-system/atoms/role';
|
||||
import { RoleSearch } from '@roleypoly/design-system/molecules/role-search';
|
||||
import { Role as RoleT } from '@roleypoly/types';
|
||||
import { sortBy, uniq } from 'lodash';
|
||||
import React from 'react';
|
||||
import { GoPlus } from 'react-icons/go';
|
||||
import { AddRoleButton, EditableRoleListStyled } from './EditableRoleList.styled';
|
||||
|
||||
type Props = {
|
||||
roles: RoleT[];
|
||||
selectedRoles: RoleT['id'][];
|
||||
unselectedRoles: RoleT[];
|
||||
onChange: (roles: RoleT['id'][]) => void;
|
||||
};
|
||||
|
||||
export const EditableRoleList = (props: Props) => {
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
|
||||
const handleRoleDelete = (role: RoleT) => () => {
|
||||
const updatedRoles = props.selectedRoles.filter((r) => r !== role.id);
|
||||
props.onChange(updatedRoles);
|
||||
};
|
||||
|
||||
const handleRoleAdd = (role: RoleT) => {
|
||||
const updatedRoles = uniq([...props.selectedRoles, role.id]);
|
||||
props.onChange(updatedRoles);
|
||||
setSearchOpen(false);
|
||||
};
|
||||
|
||||
const handleSearchOpen = () => {
|
||||
setSearchOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableRoleListStyled>
|
||||
{props.selectedRoles.length !== 0 ? (
|
||||
<>
|
||||
{sortBy(
|
||||
props.roles.filter((r) => props.selectedRoles.includes(r.id)),
|
||||
'position'
|
||||
).map((role) => (
|
||||
<Role
|
||||
key={role.id}
|
||||
role={role}
|
||||
selected={false}
|
||||
type="delete"
|
||||
onClick={handleRoleDelete(role)}
|
||||
/>
|
||||
))}
|
||||
<RoleAddButton onClick={handleSearchOpen} />
|
||||
</>
|
||||
) : (
|
||||
<RoleAddButton long onClick={handleSearchOpen} />
|
||||
)}
|
||||
<RoleSearchPopover
|
||||
isOpen={searchOpen}
|
||||
onExit={() => setSearchOpen(false)}
|
||||
unselectedRoles={props.unselectedRoles}
|
||||
onSelect={handleRoleAdd}
|
||||
/>
|
||||
</EditableRoleListStyled>
|
||||
);
|
||||
};
|
||||
|
||||
const RoleAddButton = (props: { onClick: () => void; long?: boolean }) => (
|
||||
<AddRoleButton
|
||||
data-tip="Add a role to the category"
|
||||
onClick={props.onClick}
|
||||
long={props.long}
|
||||
>
|
||||
{props.long && <>Add a role </>}
|
||||
<GoPlus />
|
||||
</AddRoleButton>
|
||||
);
|
||||
|
||||
const RoleSearchPopover = (props: {
|
||||
onSelect: (role: RoleT) => void;
|
||||
onExit: (type: string) => void;
|
||||
isOpen: boolean;
|
||||
unselectedRoles: RoleT[];
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="top left"
|
||||
active={props.isOpen}
|
||||
canDefocus
|
||||
onExit={props.onExit}
|
||||
headContent={null}
|
||||
>
|
||||
{() => (
|
||||
<RoleSearch
|
||||
onSelect={props.onSelect}
|
||||
roles={props.unselectedRoles}
|
||||
searchTerm={searchTerm}
|
||||
onSearchUpdate={setSearchTerm}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './EditableRoleList';
|
|
@ -1,16 +1,13 @@
|
|||
import { Button } from '@roleypoly/design-system/atoms/button';
|
||||
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
||||
import { Role } from '@roleypoly/design-system/atoms/role';
|
||||
import { TextInput } from '@roleypoly/design-system/atoms/text-input';
|
||||
import { Toggle } from '@roleypoly/design-system/atoms/toggle';
|
||||
import { Text } from '@roleypoly/design-system/atoms/typography';
|
||||
import { RoleSearch } from '@roleypoly/design-system/molecules/role-search';
|
||||
import { EditableRoleList } from '@roleypoly/design-system/molecules/editable-role-list';
|
||||
import { Category as CategoryT, CategoryType, Role as RoleT } from '@roleypoly/types';
|
||||
import { sortBy, uniq } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { GoHistory, GoPlus, GoTrashcan } from 'react-icons/go';
|
||||
import { GoHistory, GoTrashcan } from 'react-icons/go';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { AddRoleButton, Box, RoleContainer, Section } from './EditorCategory.styled';
|
||||
import { Box, Section } from './EditorCategory.styled';
|
||||
|
||||
export type CategoryProps = {
|
||||
title: string;
|
||||
|
@ -23,25 +20,12 @@ export type CategoryProps = {
|
|||
};
|
||||
|
||||
export const EditorCategory = (props: CategoryProps) => {
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
|
||||
const updateValue = <T extends keyof CategoryT>(key: T, value: CategoryT[T]) => {
|
||||
props.onChange({ ...props.category, [key]: value });
|
||||
};
|
||||
|
||||
const handleRoleDelete = (role: RoleT) => () => {
|
||||
const updatedRoles = props.category.roles.filter((r) => r !== role.id);
|
||||
updateValue('roles', updatedRoles);
|
||||
};
|
||||
|
||||
const handleRoleAdd = (role: RoleT) => {
|
||||
const updatedRoles = uniq([...props.category.roles, role.id]);
|
||||
updateValue('roles', updatedRoles);
|
||||
setSearchOpen(false);
|
||||
};
|
||||
|
||||
const handleSearchOpen = () => {
|
||||
setSearchOpen(true);
|
||||
const handleRoleListUpdate = (roles: RoleT['id'][]) => () => {
|
||||
updateValue('roles', roles);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -92,81 +76,15 @@ export const EditorCategory = (props: CategoryProps) => {
|
|||
<div>
|
||||
<Text>Roles</Text>
|
||||
</div>
|
||||
<RoleContainer>
|
||||
{props.roles.length > 0 ? (
|
||||
<>
|
||||
{sortBy(props.roles, 'position').map((role) => (
|
||||
<Role
|
||||
key={role.id}
|
||||
role={role}
|
||||
selected={false}
|
||||
type="delete"
|
||||
onClick={handleRoleDelete(role)}
|
||||
/>
|
||||
))}
|
||||
<RoleAddButton onClick={handleSearchOpen} tooltipId={props.category.id} />
|
||||
</>
|
||||
) : (
|
||||
<RoleAddButton
|
||||
long
|
||||
onClick={handleSearchOpen}
|
||||
tooltipId={props.category.id}
|
||||
/>
|
||||
)}
|
||||
<RoleSearchPopover
|
||||
isOpen={searchOpen}
|
||||
onExit={() => setSearchOpen(false)}
|
||||
unselectedRoles={props.unselectedRoles}
|
||||
onSelect={handleRoleAdd}
|
||||
/>
|
||||
</RoleContainer>
|
||||
<EditableRoleList
|
||||
roles={props.roles}
|
||||
unselectedRoles={props.unselectedRoles}
|
||||
selectedRoles={props.category.roles}
|
||||
onChange={handleRoleListUpdate}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<ReactTooltip id={props.category.id} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const RoleAddButton = (props: {
|
||||
onClick: () => void;
|
||||
tooltipId: string;
|
||||
long?: boolean;
|
||||
}) => (
|
||||
<AddRoleButton
|
||||
data-tip="Add a role to the category"
|
||||
data-for={props.tooltipId}
|
||||
onClick={props.onClick}
|
||||
long={props.long}
|
||||
>
|
||||
{props.long && <>Add a role </>}
|
||||
<GoPlus />
|
||||
</AddRoleButton>
|
||||
);
|
||||
|
||||
const RoleSearchPopover = (props: {
|
||||
onSelect: (role: RoleT) => void;
|
||||
onExit: (type: string) => void;
|
||||
isOpen: boolean;
|
||||
unselectedRoles: RoleT[];
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="top left"
|
||||
active={props.isOpen}
|
||||
canDefocus
|
||||
onExit={props.onExit}
|
||||
headContent={null}
|
||||
>
|
||||
{() => (
|
||||
<RoleSearch
|
||||
onSelect={props.onSelect}
|
||||
roles={props.unselectedRoles}
|
||||
searchTerm={searchTerm}
|
||||
onSearchUpdate={setSearchTerm}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { mastheadSlugs } from '@roleypoly/design-system/fixtures/storyData';
|
||||
import { GoGear } from 'react-icons/go';
|
||||
import { EditorUtilityShell } from './EditorUtilityShell';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Editor Utility Shell',
|
||||
component: EditorUtilityShell,
|
||||
args: {
|
||||
title: 'Utility Title',
|
||||
guild: mastheadSlugs[0],
|
||||
icon: <GoGear />,
|
||||
},
|
||||
};
|
||||
|
||||
export const editorUtilityShell = (args) => (
|
||||
<EditorUtilityShell {...args}>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, odit inventore?
|
||||
Recusandae dolor minima quos, laboriosam alias iusto officiis culpa! Autem, odit ut.
|
||||
Fugit quaerat esse explicabo quibusdam, ipsum maiores?
|
||||
</p>
|
||||
</EditorUtilityShell>
|
||||
);
|
|
@ -0,0 +1,60 @@
|
|||
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
|
||||
import { text900 } from '@roleypoly/design-system/atoms/typography';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const Shell = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const HeadBox = styled.div`
|
||||
${text900}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
svg {
|
||||
margin-right: 0.5em;
|
||||
position: relative;
|
||||
top: 0.125em;
|
||||
}
|
||||
|
||||
${onSmallScreen(
|
||||
css`
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
)}
|
||||
`;
|
||||
|
||||
export const Content = styled.div`
|
||||
width: 960px;
|
||||
max-width: 90vw;
|
||||
margin: 0 auto;
|
||||
padding: 1.6em 0;
|
||||
`;
|
||||
|
||||
export const Title = styled.div`
|
||||
${onSmallScreen(
|
||||
css`
|
||||
order: 2;
|
||||
flex: 1 1 100%;
|
||||
`
|
||||
)}
|
||||
`;
|
||||
|
||||
export const GoBack = styled.div`
|
||||
display: flex;
|
||||
button {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
${onSmallScreen(
|
||||
css`
|
||||
order: 1;
|
||||
flex: 1 1 100%;
|
||||
`
|
||||
)}
|
||||
`;
|
|
@ -0,0 +1,64 @@
|
|||
import { Avatar, utils as avatarUtils } from '@roleypoly/design-system/atoms/avatar';
|
||||
import { BreakpointText } from '@roleypoly/design-system/atoms/breakpoints';
|
||||
import { Button } from '@roleypoly/design-system/atoms/button';
|
||||
import {
|
||||
Content,
|
||||
GoBack,
|
||||
HeadBox,
|
||||
Shell,
|
||||
Title,
|
||||
} from '@roleypoly/design-system/molecules/editor-utility-shell/EditorUtilityShell.styled';
|
||||
import { GuildSlug } from '@roleypoly/types';
|
||||
import { GoCheck, GoReply } from 'react-icons/go';
|
||||
|
||||
export type EditorUtilityProps = {
|
||||
guildSlug: GuildSlug;
|
||||
onSubmit: <T>(output: T) => void;
|
||||
onExit: () => void;
|
||||
};
|
||||
|
||||
export const EditorUtilityShell = (
|
||||
props: EditorUtilityProps & {
|
||||
children: React.ReactNode;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
hasChanges: boolean;
|
||||
}
|
||||
) => (
|
||||
<Shell>
|
||||
<HeadBox>
|
||||
<Title>
|
||||
{props.icon}
|
||||
{props.title}
|
||||
</Title>
|
||||
<GoBack>
|
||||
{props.hasChanges ? (
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
icon={<GoCheck />}
|
||||
onClick={() => {
|
||||
props.onSubmit(undefined);
|
||||
}}
|
||||
>
|
||||
Save Changes & Exit
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="small" color="silent" icon={<GoReply />} onClick={props.onExit}>
|
||||
<BreakpointText
|
||||
large={`Go back to ${props.guildSlug.name}`}
|
||||
small="Go Back"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<Avatar
|
||||
hash={props.guildSlug.icon}
|
||||
src={avatarUtils.avatarHash(props.guildSlug.id, props.guildSlug.icon, 'icons')}
|
||||
>
|
||||
{avatarUtils.initialsFromName(props.guildSlug.name)}
|
||||
</Avatar>
|
||||
</GoBack>
|
||||
</HeadBox>
|
||||
<Content>{props.children}</Content>
|
||||
</Shell>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from './EditorUtilityShell';
|
|
@ -0,0 +1,13 @@
|
|||
import { presentableGuild, roleypolyGuild } from '../../fixtures/storyData';
|
||||
import { EditorAccessControl } from './EditorAccessControl';
|
||||
|
||||
export default {
|
||||
title: 'Organisms/Editor/Access Control',
|
||||
component: EditorAccessControl,
|
||||
args: {
|
||||
guild: presentableGuild,
|
||||
guildSlug: roleypolyGuild,
|
||||
},
|
||||
};
|
||||
|
||||
export const accessControl = (args) => <EditorAccessControl {...args} />;
|
|
@ -0,0 +1,8 @@
|
|||
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const RoleContainer = styled.div`
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${palette.taupe400};
|
||||
padding: 10px;
|
||||
`;
|
|
@ -0,0 +1,185 @@
|
|||
import { IconHelper } from '@roleypoly/design-system/atoms/icon-helper';
|
||||
import { Space } from '@roleypoly/design-system/atoms/space';
|
||||
import { Toggle } from '@roleypoly/design-system/atoms/toggle';
|
||||
import {
|
||||
AmbientLarge,
|
||||
LargeText,
|
||||
Link,
|
||||
Text,
|
||||
} from '@roleypoly/design-system/atoms/typography';
|
||||
import { EditableRoleList } from '@roleypoly/design-system/molecules/editable-role-list';
|
||||
import {
|
||||
EditorUtilityProps,
|
||||
EditorUtilityShell,
|
||||
} from '@roleypoly/design-system/molecules/editor-utility-shell';
|
||||
import { GuildAccessControl, PresentableGuild } from '@roleypoly/types';
|
||||
import deepEqual from 'deep-equal';
|
||||
import * as React from 'react';
|
||||
import { GoAlert, GoInfo, GoShield, GoThumbsdown, GoThumbsup } from 'react-icons/go';
|
||||
import { RoleContainer } from './EditorAccessControl.styled';
|
||||
|
||||
export type EditorAccessControlProps = {
|
||||
guild: PresentableGuild;
|
||||
} & EditorUtilityProps;
|
||||
|
||||
export const EditorAccessControl = (props: EditorAccessControlProps) => {
|
||||
const [accessControl, setAccessControl] = React.useState(
|
||||
props.guild.data.accessControl
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAccessControl(props.guild.data.accessControl);
|
||||
}, [props.guild.data.accessControl]);
|
||||
|
||||
const onSubmit = () => {
|
||||
props.onSubmit(accessControl);
|
||||
};
|
||||
|
||||
const handleChange =
|
||||
(key: keyof GuildAccessControl) =>
|
||||
(value: GuildAccessControl[keyof GuildAccessControl]) => {
|
||||
setAccessControl((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const hasChanges = React.useMemo(() => {
|
||||
return !deepEqual(accessControl, props.guild.data.accessControl);
|
||||
}, [accessControl, props.guild.data.accessControl]);
|
||||
|
||||
const rolesNotInBlocked = React.useMemo(() => {
|
||||
return props.guild.roles.filter(
|
||||
(role) => role.id !== props.guild.id && !accessControl.blockList.includes(role.id)
|
||||
);
|
||||
}, [accessControl, props.guild.roles]);
|
||||
|
||||
const rolesNotInAllowed = React.useMemo(() => {
|
||||
return props.guild.roles.filter(
|
||||
(role) => role.id !== props.guild.id && !accessControl.allowList.includes(role.id)
|
||||
);
|
||||
}, [accessControl, props.guild.roles]);
|
||||
|
||||
return (
|
||||
<EditorUtilityShell
|
||||
guildSlug={props.guild.guild}
|
||||
title="Access Control"
|
||||
icon={<GoShield />}
|
||||
hasChanges={hasChanges}
|
||||
onSubmit={onSubmit}
|
||||
onExit={props.onExit}
|
||||
>
|
||||
<p>
|
||||
<IconHelper level="chrome">
|
||||
<GoInfo />
|
||||
</IconHelper>
|
||||
Admins and Role Managers are exempt from all of these limits. Please note,
|
||||
this settings page is in order of precedence.
|
||||
</p>
|
||||
<Space />
|
||||
<div>
|
||||
<LargeText>
|
||||
Block pending members from using Roleypoly
|
||||
<IconHelper level="error">
|
||||
<GoThumbsdown />
|
||||
</IconHelper>
|
||||
</LargeText>
|
||||
<br />
|
||||
<br />
|
||||
{/* <RoleContainer> */}
|
||||
<Toggle
|
||||
state={accessControl.blockPending}
|
||||
onChange={handleChange('blockPending')}
|
||||
>
|
||||
If a user is behind Discord's{' '}
|
||||
<Link href="https://support.discord.com/hc/en-us/articles/1500000466882-Rules-Screening-FAQ">
|
||||
Membership Screening
|
||||
</Link>{' '}
|
||||
feature, they can <b>not</b> use Roleypoly.
|
||||
</Toggle>
|
||||
{/* </RoleContainer> */}
|
||||
<p>
|
||||
<AmbientLarge>
|
||||
<IconHelper level="chrome">
|
||||
<GoInfo />
|
||||
</IconHelper>
|
||||
This only applies to Discord servers with Community features enabled.
|
||||
</AmbientLarge>
|
||||
</p>
|
||||
</div>
|
||||
<Space />
|
||||
<div>
|
||||
<p>
|
||||
<LargeText>
|
||||
Block roles from using Roleypoly
|
||||
<IconHelper level="error">
|
||||
<GoThumbsdown />
|
||||
</IconHelper>
|
||||
</LargeText>
|
||||
<br />
|
||||
<Text>
|
||||
If there are roles in this list, any server member <b>with</b> a role in the
|
||||
list can <b>not</b> use Roleypoly.
|
||||
<br />
|
||||
<IconHelper level="info">
|
||||
<GoInfo />
|
||||
</IconHelper>
|
||||
Blocked roles take precedence over the allowed roles.
|
||||
</Text>
|
||||
</p>
|
||||
<RoleContainer>
|
||||
<EditableRoleList
|
||||
roles={props.guild.roles}
|
||||
unselectedRoles={rolesNotInBlocked}
|
||||
selectedRoles={accessControl.blockList}
|
||||
onChange={handleChange('blockList')}
|
||||
/>
|
||||
</RoleContainer>
|
||||
<p>
|
||||
<AmbientLarge>
|
||||
<IconHelper level="chrome">
|
||||
<GoInfo />
|
||||
</IconHelper>
|
||||
If your Discord server has a "muted" or "visitor" role, this setting is
|
||||
meant to complement it.
|
||||
</AmbientLarge>
|
||||
</p>
|
||||
</div>
|
||||
<Space />
|
||||
<div>
|
||||
<p>
|
||||
<LargeText>
|
||||
Allow these roles to use Roleypoly
|
||||
<IconHelper level="success">
|
||||
<GoThumbsup />
|
||||
</IconHelper>
|
||||
</LargeText>
|
||||
<br />
|
||||
<Text>
|
||||
If there are roles in this list, any server member <b>without</b> a role in
|
||||
the list can <b>not</b> use Roleypoly.
|
||||
<br />
|
||||
<IconHelper level="warn">
|
||||
<GoAlert />
|
||||
</IconHelper>
|
||||
This can disrupt use of the bot, so be careful!
|
||||
</Text>
|
||||
</p>
|
||||
<RoleContainer>
|
||||
<EditableRoleList
|
||||
roles={props.guild.roles}
|
||||
unselectedRoles={rolesNotInAllowed}
|
||||
selectedRoles={accessControl.allowList}
|
||||
onChange={handleChange('allowList')}
|
||||
/>
|
||||
</RoleContainer>
|
||||
<p>
|
||||
<AmbientLarge>
|
||||
<IconHelper level="chrome">
|
||||
<GoInfo />
|
||||
</IconHelper>
|
||||
If your Discord server uses a "role gating" system, this setting is
|
||||
meant to complement it.
|
||||
</AmbientLarge>
|
||||
</p>
|
||||
</div>
|
||||
</EditorUtilityShell>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './EditorAccessControl';
|
|
@ -0,0 +1,26 @@
|
|||
import ReactTooltip from 'react-tooltip';
|
||||
import { BreakpointsProvider } from '../../atoms/breakpoints';
|
||||
import { guildEnum, mastheadSlugs, roleypolyGuild, user } from '../../fixtures/storyData';
|
||||
import { EditorAccessControlTemplate } from './EditorAccessControl';
|
||||
|
||||
export default {
|
||||
title: 'Templates/Editor Access Control',
|
||||
component: EditorAccessControlTemplate,
|
||||
decorators: [
|
||||
(story) => (
|
||||
<BreakpointsProvider>
|
||||
{story()}
|
||||
<ReactTooltip />
|
||||
</BreakpointsProvider>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
errors: { validationStatus: 0 },
|
||||
guilds: mastheadSlugs,
|
||||
user: user,
|
||||
guild: guildEnum.guilds[1],
|
||||
guildSlug: roleypolyGuild,
|
||||
},
|
||||
};
|
||||
|
||||
export const editorAccessControl = (args) => <EditorAccessControlTemplate {...args} />;
|
|
@ -0,0 +1,2 @@
|
|||
import styled from 'styled-components';
|
||||
export const EditorAccessControlStyled = styled.div``;
|
|
@ -0,0 +1,21 @@
|
|||
import { AppShell, AppShellProps } from '@roleypoly/design-system/organisms/app-shell';
|
||||
import {
|
||||
EditorAccessControl,
|
||||
EditorAccessControlProps,
|
||||
} from '@roleypoly/design-system/organisms/editor-access-control';
|
||||
|
||||
export const EditorAccessControlTemplate = (
|
||||
props: EditorAccessControlProps & Omit<AppShellProps, 'children'>
|
||||
) => {
|
||||
const { guildSlug, guild, onSubmit, onExit, ...appShellProps } = props;
|
||||
return (
|
||||
<AppShell {...appShellProps} activeGuildId={guild.id} small>
|
||||
<EditorAccessControl
|
||||
guildSlug={guildSlug}
|
||||
guild={guild}
|
||||
onSubmit={onSubmit}
|
||||
onExit={onExit}
|
||||
/>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './EditorAccessControl';
|
|
@ -1,8 +0,0 @@
|
|||
import { EditorUtility } from './EditorUtility';
|
||||
|
||||
export default {
|
||||
title: 'Templates/Editor Utility',
|
||||
component: EditorUtility,
|
||||
};
|
||||
|
||||
export const editorUtility = (args) => <EditorUtility {...args} />;
|
|
@ -1,2 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
export const EditorUtilityStyled = styled.div``;
|
|
@ -1,5 +0,0 @@
|
|||
import { EditorUtilityStyled } from './EditorUtility.styled';
|
||||
|
||||
export const EditorUtility = () => (
|
||||
<EditorUtilityStyled>EditorUtility</EditorUtilityStyled>
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
export * from './EditorUtility';
|
|
@ -42,6 +42,11 @@ export const errorMessages: { [code: string]: ErrorMessage } = {
|
|||
japanese: `...but it didn't believe me. :( ごめんなさい`,
|
||||
friendlyCode: 'Yo.',
|
||||
},
|
||||
accessControlViolation: {
|
||||
english: `You're not allowed to pick roles on that server.`,
|
||||
japanese: `このサーバーでは許可されていない権限です`,
|
||||
friendlyCode: 'Hold up!',
|
||||
},
|
||||
};
|
||||
|
||||
export const getMessageFromCode = (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue