feat: add access control

This commit is contained in:
41666 2021-07-18 01:57:03 -04:00
parent 9c07ff0e54
commit 3f45153b66
47 changed files with 1084 additions and 164 deletions

View file

@ -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} />;

View file

@ -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;
`};
`;

View file

@ -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&nbsp;&nbsp;</>}
<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>
);
};

View file

@ -0,0 +1 @@
export * from './EditableRoleList';

View file

@ -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&nbsp;&nbsp;</>}
<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>
);
};

View file

@ -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>
);

View file

@ -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%;
`
)}
`;

View file

@ -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>
);

View file

@ -0,0 +1 @@
export * from './EditorUtilityShell';