feat(Editor): make server utilities their own pages

Signed-off-by: Katalina Okano <git@kat.cafe>
This commit is contained in:
41666 2021-07-17 19:23:35 -04:00
parent d52508a046
commit 4cc202b62a
8 changed files with 125 additions and 153 deletions

View file

@ -5,10 +5,10 @@ export const Space = styled.div`
height: 15px; height: 15px;
`; `;
export const LinedSpace = styled.div<{ width?: number }>` export const LinedSpace = styled.div<{ width?: number; color?: string }>`
height: 7.5px; height: 7.5px;
margin-top: 7.5px; margin-top: 7.5px;
border-top: 1px solid ${palette.taupe300}; border-top: 1px solid ${(props) => (props.color ? props.color : palette.taupe300)};
${(props) => ${(props) =>
props.width && props.width &&
css` css`

View file

@ -0,0 +1,12 @@
import { guildData } from '@roleypoly/design-system/fixtures/storyData';
import { ServerUtilities } from './ServerUtilities';
export default {
title: 'Molecules/Server Utilities',
component: ServerUtilities,
args: {
guildData: guildData,
},
};
export const serverUtilities = (args) => <ServerUtilities {...args} />;

View file

@ -0,0 +1,37 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import { text200, text400 } from '@roleypoly/design-system/atoms/typography';
import styled from 'styled-components';
export const ClickaleBlock = styled.a`
display: flex;
color: unset;
text-decoration: none;
align-items: center;
padding: 0.5em;
transition: background-color ${transitions.actionable}s ease-in-out;
box-sizing: border-box;
justify-content: space-between;
max-width: 93vw;
:hover {
background-color: ${palette.taupe300};
}
`;
export const Title = styled.div`
${text400};
/* padding: 0.15em 0; */
svg {
color: ${palette.taupe500};
position: relative;
top: 2px;
}
`;
export const Description = styled.div`
${text200};
`;
export const MainSide = styled.div``;

View file

@ -1,72 +1,74 @@
import { palette } from '@roleypoly/design-system/atoms/colors'; import {
import { FaderOpacity } from '@roleypoly/design-system/atoms/fader'; ClickaleBlock,
import { TextInput } from '@roleypoly/design-system/atoms/text-input'; Description,
import { AmbientLarge, Text } from '@roleypoly/design-system/atoms/typography'; MainSide,
import { MessageBox } from '@roleypoly/design-system/organisms/role-picker/RolePicker.styled'; Title,
import { GuildData, WebhookValidationStatus } from '@roleypoly/types'; } from '@roleypoly/design-system/molecules/server-utilities/ServerUtilities.styled';
import { GoAlert, GoInfo } from 'react-icons/go'; import { GuildData } from '@roleypoly/types';
import ReactTooltip from 'react-tooltip'; import { GoArchive, GoChevronRight, GoReport, GoShield, GoSync } from 'react-icons/go';
import styled from 'styled-components';
type Props = { type Props = {
onChange: (guildData: GuildData) => void;
guildData: GuildData; guildData: GuildData;
validationStatus: WebhookValidationStatus | null;
}; };
export const ServerUtilities = (props: Props) => { const Utility = (props: {
return ( link: string;
<MessageBox> title: React.ReactNode;
<Text> description: string;
(optional) Webhook URL for Audit Logging{' '} }) => (
<GoInfo <ClickaleBlock href={props.link}>
data-for="server-utilities" <MainSide>
data-tip="Reports changes made in the editor to a Webhook integration within your Discord server." <Title>{props.title}</Title>
/> <Description>{props.description}</Description>
</Text> </MainSide>
<TextInput <div>
placeholder="https://discord.com/api/webhooks/000000000000000000/..." <GoChevronRight />
value={props.guildData.auditLogWebhook || ''} </div>
onChange={(event) => </ClickaleBlock>
props.onChange({ ...props.guildData, auditLogWebhook: event.target.value }) );
}
/>
<FaderOpacity isVisible={props.validationStatus !== WebhookValidationStatus.Ok}>
<ValidationStatus validationStatus={props.validationStatus} />
</FaderOpacity>
<ReactTooltip id="server-utilities" />
</MessageBox>
);
};
const ValidationStatus = (props: Pick<Props, 'validationStatus'>) => { export const ServerUtilities = (props: Props) => (
switch (props.validationStatus) { <div>
case WebhookValidationStatus.NotDiscordURL: {/* <LargeText>Server Utilities</LargeText> */}
return ( <Utility
<AmbientLarge> title={
<Alert /> URL must be to a Discord webhook, starting with <>
"https://discord.com/api/webhooks/". <GoShield />
</AmbientLarge> &nbsp;&nbsp;Access Control
); </>
case WebhookValidationStatus.NotSameGuild: }
return ( description="Set up who can use Roleypoly in your server"
<AmbientLarge> link={`/s/${props.guildData.id}/edit/access-control`}
<Alert /> Webhook must be in the same guild you are currently editing. />
</AmbientLarge> <Utility
); title={
case WebhookValidationStatus.DoesNotExist: <>
return ( <GoReport />
<AmbientLarge> &nbsp;&nbsp;Audit Logging
<Alert /> This webhook doesn't exist. </>
</AmbientLarge> }
); description="Setup audit logging via a Discord webhook"
default: link={`/s/${props.guildData.id}/edit/audit-logging`}
return <AmbientLarge>&nbsp;</AmbientLarge>; />
} <Utility
}; title={
<>
const Alert = styled(GoAlert)` <GoSync />
color: ${palette.red400}; &nbsp;&nbsp;Import from Roleypoly Legacy
position: relative; </>
top: 2px; }
`; description="Used Roleypoly before and don't see your categories?"
link={`/s/${props.guildData.id}/edit/import-from-legacy`}
/>
<Utility
title={
<>
<GoArchive />
&nbsp;&nbsp;Manage your Data
</>
}
description="Export or delete all of your Roleypoly data."
link={`/s/${props.guildData.id}/edit/data`}
/>
</div>
);

View file

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

View file

@ -5,12 +5,7 @@ import { ServerUtilities } from '@roleypoly/design-system/molecules/server-utili
import { SecondaryEditing } from '@roleypoly/design-system/organisms/masthead'; import { SecondaryEditing } from '@roleypoly/design-system/organisms/masthead';
import { Container } from '@roleypoly/design-system/organisms/role-picker/RolePicker.styled'; import { Container } from '@roleypoly/design-system/organisms/role-picker/RolePicker.styled';
import { ServerCategoryEditor } from '@roleypoly/design-system/organisms/server-category-editor'; import { ServerCategoryEditor } from '@roleypoly/design-system/organisms/server-category-editor';
import { import { Category, PresentableGuild } from '@roleypoly/types';
Category,
GuildData,
PresentableGuild,
WebhookValidationStatus,
} from '@roleypoly/types';
import deepEqual from 'deep-equal'; import deepEqual from 'deep-equal';
import React from 'react'; import React from 'react';
@ -19,26 +14,17 @@ export type EditorShellProps = {
onGuildChange?: (guild: PresentableGuild) => void; onGuildChange?: (guild: PresentableGuild) => void;
onCategoryChange?: (category: Category) => void; onCategoryChange?: (category: Category) => void;
onMessageChange?: (message: PresentableGuild['data']['message']) => void; onMessageChange?: (message: PresentableGuild['data']['message']) => void;
errors: {
webhookValidation: WebhookValidationStatus;
};
}; };
export const EditorShell = (props: EditorShellProps) => { export const EditorShell = (props: EditorShellProps) => {
const [guild, setGuild] = React.useState<PresentableGuild>(props.guild); const [guild, setGuild] = React.useState<PresentableGuild>(props.guild);
const [errors, setErrors] = React.useState<EditorShellProps['errors']>(props.errors);
React.useEffect(() => { React.useEffect(() => {
setGuild(props.guild); setGuild(props.guild);
}, [props.guild]); }, [props.guild]);
React.useEffect(() => {
setErrors(props.errors);
}, [props.errors]);
const reset = () => { const reset = () => {
setGuild(props.guild); setGuild(props.guild);
setErrors({ webhookValidation: WebhookValidationStatus.Ok });
}; };
const replaceCategories = (categories: Category[]) => { const replaceCategories = (categories: Category[]) => {
@ -53,12 +39,6 @@ export const EditorShell = (props: EditorShellProps) => {
}); });
}; };
const updateGuildData = (data: PresentableGuild['data']) => {
setGuild((currentGuild) => {
return { ...currentGuild, data };
});
};
const doSubmit = () => { const doSubmit = () => {
props.onGuildChange?.(guild); props.onGuildChange?.(guild);
}; };
@ -87,47 +67,8 @@ export const EditorShell = (props: EditorShellProps) => {
<Space /> <Space />
<ServerCategoryEditor guild={guild} onChange={replaceCategories} /> <ServerCategoryEditor guild={guild} onChange={replaceCategories} />
<LinedSpace /> <LinedSpace />
<ServerUtilities <ServerUtilities guildData={guild.data} />
guildData={guild.data}
onChange={updateGuildData}
validationStatus={validateWebhook(
guild.data.auditLogWebhook,
errors.webhookValidation
)}
/>
</Container> </Container>
</> </>
); );
}; };
const validateWebhook = (
webhook: GuildData['auditLogWebhook'],
validationStatus: WebhookValidationStatus
) => {
if (!webhook) {
return WebhookValidationStatus.NoneSet;
}
try {
const url = new URL(webhook);
if (
url.hostname !== 'discord.com' ||
url.protocol !== 'https:' ||
url.pathname.startsWith('api/webhooks/')
) {
return WebhookValidationStatus.NotDiscordURL;
}
} catch (e) {
return WebhookValidationStatus.Ok;
}
if (
validationStatus !== WebhookValidationStatus.Ok &&
validationStatus !== WebhookValidationStatus.NoneSet
) {
return validationStatus;
}
return WebhookValidationStatus.Ok;
};

View file

@ -11,9 +11,7 @@ export const EditorTemplate = (
props; props;
return ( return (
<AppShell {...appShellProps} activeGuildId={guild.id} small double> <AppShell {...appShellProps} activeGuildId={guild.id} small double>
<EditorShell guild={guild} onGuildChange={onGuildChange} errors={props.errors} /> <EditorShell guild={guild} onGuildChange={onGuildChange} />
</AppShell> </AppShell>
); );
}; };
export type EditorErrors = EditorShellProps['errors'];

View file

@ -1,11 +1,10 @@
import { navigate, Redirect } from '@reach/router'; import { navigate, Redirect } from '@reach/router';
import { EditorErrors, EditorTemplate } from '@roleypoly/design-system/templates/editor'; import { EditorTemplate } from '@roleypoly/design-system/templates/editor';
import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading';
import { import {
GuildDataUpdate, GuildDataUpdate,
PresentableGuild, PresentableGuild,
UserGuildPermissions, UserGuildPermissions,
WebhookValidationStatus,
} from '@roleypoly/types'; } from '@roleypoly/types';
import * as React from 'react'; import * as React from 'react';
import { useAppShellProps } from '../contexts/app-shell/AppShellContext'; import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
@ -26,9 +25,6 @@ const Editor = (props: EditorProps) => {
const [guild, setGuild] = React.useState<PresentableGuild | null | false>(null); const [guild, setGuild] = React.useState<PresentableGuild | null | false>(null);
const [pending, setPending] = React.useState(false); const [pending, setPending] = React.useState(false);
const [errors, setErrors] = React.useState<EditorErrors>({
webhookValidation: WebhookValidationStatus.Ok,
});
React.useEffect(() => { React.useEffect(() => {
const shouldPullUncached = (): boolean => { const shouldPullUncached = (): boolean => {
@ -104,28 +100,13 @@ const Editor = (props: EditorProps) => {
navigate(`/s/${props.serverID}`); navigate(`/s/${props.serverID}`);
} }
if (response.status === 400) {
const error = await response.json();
if (error.data.what === 'webhookValidationStatus') {
setErrors((errors) => ({
...errors,
webhookValidation: error.data.webhookValidationStatus,
}));
}
}
setPending(false); setPending(false);
}; };
return ( return (
<> <>
<Title title={`Editing ${guild.guild.name} - Roleypoly`} /> <Title title={`Editing ${guild.guild.name} - Roleypoly`} />
<EditorTemplate <EditorTemplate {...appShellProps} guild={guild} onGuildChange={onGuildChange} />
{...appShellProps}
guild={guild}
onGuildChange={onGuildChange}
errors={errors}
/>
</> </>
); );
}; };