Add REST Error Dialogs

This commit is contained in:
ADAMJR 2021-09-28 15:39:13 +01:00
parent 0a542c5829
commit 47e0f99253
14 changed files with 142 additions and 131 deletions

View File

@ -2,7 +2,6 @@ import { useState } from 'react';
import { ContextMenuTrigger } from 'react-contextmenu';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router';
import Select from 'react-select';
import { updateChannel } from '../../../store/channels';
import { getGuildRoles } from '../../../store/guilds';
import RoleMenu from '../../ctx-menus/role-menu';
@ -11,6 +10,7 @@ import Category from '../../utils/category';
import SaveChanges from '../../utils/save-changes';
import TabLink from '../../utils/tab-link';
import PermOverrides from './perm-overrides';
import ScarceSelect from './scarce-select';
const ChannelSettingsPerms: React.FunctionComponent = () => {
const { guildId }: any = useParams();
@ -19,71 +19,37 @@ const ChannelSettingsPerms: React.FunctionComponent = () => {
const byPosition = (a, b) => (a.position > b.position) ? 1 : -1;
const allRoles = useSelector(getGuildRoles(guildId)).sort(byPosition);
const [overrides, setOverrides] = useState(channel.overrides ?? []);
const [activeOverride, setOverride] = useState(channel.overrides?.[0]);
const unaddedRoles = allRoles.filter(r => !overrides.some(o => o.roleId === r.id));
const overrideRoles = allRoles.filter(r => overrides.some(o => o.roleId === r.id));
const unaddedRoles = allRoles.filter(r => !channel.overrides?.some(o => o.roleId === r.id));
const overrideRoles = allRoles.filter(r => !channel.overrides?.some(o => o.roleId === r.id));
const [activeRoleId, setActiveRoleId] = useState(overrideRoles[0]?.id ?? '');
const activeOverride = overrides.find(o => o.roleId === activeRoleId);
const deleteActiveOverride = () => {
const index = overrides?.findIndex(o => o.roleId === activeRoleId);
overrides.splice(index, 1);
setOverrides(overrides);
setOverride({ allow: 0, deny: 0, roleId: activeRoleId });
setActiveRoleId('');
}
const RoleDetails = () => {
return (activeOverride) ? (
return (
<>
<PermOverrides
overrides={overrides}
setOverrides={setOverrides}
setOverride={setOverride}
activeOverride={activeOverride} />
<NormalButton
onClick={deleteActiveOverride}
className="bg-danger float-right"
type="button">Delete</NormalButton>
</>
) : null;
);
}
const onSave = (e) => {
const filtered = overrides.filter(o => o.allow + o.deny > 0);
dispatch(updateChannel(channel.id, { overrides: filtered }));
};
const overrides = channel.overrides ?? [];
if (activeOverride && activeOverride.allow + activeOverride.deny > 0)
overrides.push(activeOverride);
const colorStyles = {
singleValue: () => ({ display: 'none' }),
control: () => ({
width: '100%',
backgroundColor: 'var(--bg-secondary)',
borderRadius: '5px',
}),
option: (styles, { data }) => ({
...styles,
color: data.color,
backgroundColor: 'var(--bg-secondary)',
cursor: 'pointer',
}),
input: (styles) => ({ ...styles, color: 'var(--font)' }),
menu: (styles) => ({
...styles,
backgroundColor: 'var(--bg-secondary)',
}),
multiValue: (styles) => ({
...styles,
color: 'var(--font)',
backgroundColor: 'var(--bg-tertiary)',
}),
indicatorSeparator: () => ({ display: 'none' }),
indicatorsContainer: (styles) => ({ ...styles, float: 'right' }),
multiValueLabel: (styles, { data }) => ({
...styles,
color: data.color,
backgroundColor: 'var(--bg-tertiary)',
}),
dispatch(updateChannel(channel.id, { overrides }));
};
return (
@ -103,29 +69,23 @@ const ChannelSettingsPerms: React.FunctionComponent = () => {
))}
<Category className="muted m-1 mt-3" title="Add Role" />
<Select
placeholder="Add role..."
options={unaddedRoles.map(r => ({
label: r.name,
value: r.id,
color: r.color,
}))}
styles={colorStyles}
<ScarceSelect
mapOptions={r => ({ label: r.name, value: r.id, color: r.color })}
onChange={select => {
const roleId = select.value;
setOverrides(overrides.concat({ allow: 0, deny: 0, roleId }));
setOverride({ allow: 0, deny: 0, roleId });
setActiveRoleId(roleId);
}}
noOptionsMessage={() => 'All roles have been added'} />
}}
unadded={unaddedRoles} />
</nav>
</div>
<div className="lg:col-span-9 col-span-12">
{overrides && <RoleDetails />}
{activeOverride && <RoleDetails />}
</div>
<SaveChanges
onSave={onSave}
obj={{ overrides }} />
obj={{ overrides: activeOverride }} />
</div>
);
}

View File

@ -8,20 +8,24 @@ import Category from '../../utils/category';
import ThreeToggle from '../../utils/input/three-toggle';
export interface PermOverrides {
setOverrides: React.Dispatch<React.SetStateAction<ChannelTypes.Override[]>>;
overrides: ChannelTypes.Override[];
activeOverride: ChannelTypes.Override;
setOverride: React.Dispatch<React.SetStateAction<ChannelTypes.Override | undefined>>;
activeOverride: ChannelTypes.Override | undefined;
}
const PermOverrides: React.FunctionComponent<PermOverrides> = ({ setOverrides, overrides, activeOverride }) => {
const PermOverrides: React.FunctionComponent<PermOverrides> = ({ setOverride, activeOverride }) => {
const dispatch = useDispatch();
const { description } = usePerms();
const [allow, setAllow] = useState(activeOverride.allow);
const [deny, setDeny] = useState(activeOverride.deny);
const [allow, setAllow] = useState(activeOverride?.allow ?? 0);
const [deny, setDeny] = useState(activeOverride?.deny ?? 0);
if (!activeOverride) return null;
const category = 'text';
const togglePerm = (name: string, state: string) => {
// FIXME: initial state twice (off), then other states
// console.log(state);
if (state === 'indeterminate') {
setAllow(allow & ~PermissionTypes.All[name]);
setDeny(deny & ~PermissionTypes.All[name]);
@ -32,16 +36,18 @@ const PermOverrides: React.FunctionComponent<PermOverrides> = ({ setOverrides, o
setAllow(allow & ~PermissionTypes.All[name]);
setDeny(deny | PermissionTypes.All[name]);
}
// console.log('allow', allow);
// console.log('deny', deny);
updateOverrides();
}
const updateOverrides = () => {
const roleId = activeOverride.roleId;
const newOverrides = [...overrides];
const thisIndex = newOverrides.findIndex(o => o.roleId === roleId);
newOverrides[thisIndex] = { allow, deny, roleId };
activeOverride.allow = allow;
activeOverride.deny = deny;
// setOverrides(newOverrides);
dispatch(openSaveChanges(true));
setOverride(activeOverride);
// FIXME: this is rerendering the toggles, which messes up their state
// dispatch(openSaveChanges(true));
};
const isAllowed = (name: string) => Boolean(allow & PermissionTypes.All[name]);

View File

@ -0,0 +1,52 @@
import Select from 'react-select';
interface ScarceSelectProps {
mapOptions: (entity: any) => { label: string, value: string, color: string };
onChange: (select: HTMLSelectElement) => void;
unadded: any[];
}
const ScarceSelect: React.FunctionComponent<ScarceSelectProps> = (props) => {
const colorStyles = {
singleValue: () => ({ display: 'none' }),
control: () => ({
width: '100%',
backgroundColor: 'var(--bg-secondary)',
borderRadius: '5px',
}),
option: (styles, { data }) => ({
...styles,
color: data.color,
backgroundColor: 'var(--bg-secondary)',
cursor: 'pointer',
}),
input: (styles) => ({ ...styles, color: 'var(--font)' }),
menu: (styles) => ({
...styles,
backgroundColor: 'var(--bg-secondary)',
}),
multiValue: (styles) => ({
...styles,
color: 'var(--font)',
backgroundColor: 'var(--bg-tertiary)',
}),
indicatorSeparator: () => ({ display: 'none' }),
indicatorsContainer: (styles) => ({ ...styles, float: 'right' }),
multiValueLabel: (styles, { data }) => ({
...styles,
color: data.color,
backgroundColor: 'var(--bg-tertiary)',
}),
};
return (
<Select
placeholder="Add role..."
options={props.unadded.map(props.mapOptions)}
styles={colorStyles}
onChange={props.onChange}
noOptionsMessage={() => 'All roles have been added'} />
);
}
export default ScarceSelect;

View File

@ -1,3 +1,4 @@
import { useSnackbar } from 'notistack';
import { useForm } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { toggleDevMode } from '../../../store/config';
@ -14,6 +15,7 @@ const UserSettingsOverview: React.FunctionComponent = () => {
const user = useSelector((s: Store.AppState) => s.auth.user)!;
const { register, handleSubmit, setValue } = useForm();
const devMode = useSelector((s: Store.AppState) => s.config.devMode);
const { enqueueSnackbar } = useSnackbar();
const onSave = (e) => {
const onUpdate = (payload) => dispatch(updateSelf(payload));
@ -47,6 +49,10 @@ const UserSettingsOverview: React.FunctionComponent = () => {
name="email"
register={register}
options={{ value: user.email }} />
<NormalButton
onClick={() => enqueueSnackbar({
})}>Verify</NormalButton>
</div>
<div className="pt-5">

View File

@ -8,7 +8,6 @@ import CreateInvite from '../modals/create-invite';
import GuildSettings from '../modals/guild-settings/guild-settings';
import UserProfile from '../modals/user-profile';
import UserSettings from '../modals/user-settings/user-settings';
import UIDialog from '../utils/ui-dialog';
import WSListener from '../ws-listener';
export type PageWrapperProps = React.DetailedHTMLProps<
@ -31,7 +30,6 @@ const PageWrapper: React.FunctionComponent<PageWrapperProps> = (props) => {
{...props}>
{props.children}
<WSListener />
<UIDialog />
{/* modals */}
<CreateChannel />
<CreateGuild />

View File

@ -0,0 +1,16 @@
input ~ .dot {
transition: 0.3s ease-in-out !important;
}
input[value='on'] ~ .dot {
transform: translateX(100%);
background-color: var(--success);
}
input[value='off'] ~ .dot {
transform: translateX(0%);
background-color: var(--danger);
}
input[value='indeterminate'] ~ .dot {
background-color: #ccc;
transform: translateX(50%);
}

View File

@ -1,4 +1,4 @@
import './toggle.scoped.css';
import './three-toggle.scoped.css';
import classNames from 'classnames';
import { useEffect } from 'react';
@ -31,8 +31,6 @@ const ThreeToggle: React.FunctionComponent<ThreeToggleProps> = (props) => {
else if (checkbox.value === 'off')
checkbox.setAttribute('value', 'on');
else checkbox.setAttribute('value', 'off');
console.log(checkbox.value);
}}
type="checkbox"
className="sr-only" />

View File

@ -2,15 +2,7 @@ input ~ .dot {
transition: 0.3s ease-in-out !important;
}
input[value='on'] ~ .dot {
input:checked ~ .dot {
transform: translateX(100%);
background-color: var(--success);
}
input[value='off'] ~ .dot {
transform: translateX(0%);
background-color: var(--danger);
}
input[value='indeterminate'] ~ .dot {
background-color: #ccc;
transform: translateX(50%);
}

View File

@ -1,25 +0,0 @@
import { useSnackbar } from 'notistack';
import { FunctionComponent, useEffect } from 'react';
import { useSelector } from 'react-redux';
const UIDialog: FunctionComponent = () => {
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const dialog = useSelector((s: Store.AppState) => s.ui.openDialog);
useEffect(() => {
if (!dialog) return closeSnackbar('dialog');
if (dialog.content === 'User not logged in') return;
enqueueSnackbar({
anchorOrigin: { vertical: 'bottom', horizontal: 'center' },
content: dialog.content,
key: 'dialog',
persist: true,
variant: dialog.variant,
});
}, [dialog]);
return null;
}
export default UIDialog;

View File

@ -4,17 +4,18 @@ import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { actions as users, getUser } from '../store/users';
import { actions as meta } from '../store/meta';
import { actions as uiActions } from '../store/ui';
import { actions as uiActions, Dialog } from '../store/ui';
import { actions as invites } from '../store/invites';
import { actions as members, getSelfMember } from '../store/members';
import { actions as roles } from '../store/roles';
import { actions as typing } from '../store/typing';
import { actions as guilds, getGuild } from '../store/guilds';
import { actions as guilds } from '../store/guilds';
import { actions as messages } from '../store/messages';
import { actions as channels } from '../store/channels';
import { actions as auth, logoutUser } from '../store/auth';
import { actions as pings, addPing } from '../store/pings';
import { useSnackbar } from 'notistack';
import events from '../services/event-service';
const WSListener: React.FunctionComponent = () => {
const dispatch = useDispatch();
@ -29,12 +30,23 @@ const WSListener: React.FunctionComponent = () => {
useEffect(() => {
if (hasListened) return;
ws.on('error', (error: any) => {
const handleError = (error: any) => {
enqueueSnackbar(`${error.data?.message ?? error.message}.`, {
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
variant: 'error',
autoHideDuration: 5000,
});
}
ws.on('error', handleError);
events.on('dialog', (dialog: Dialog) => {
console.log(dialog);
enqueueSnackbar(dialog.content, {
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
variant: dialog.variant,
autoHideDuration: 5000,
});
});
// add channel to guilds.channels

View File

@ -0,0 +1,5 @@
import { EventEmitter } from 'events';
const events = new EventEmitter();
export default events;

View File

@ -30,7 +30,7 @@ export default store => next => async action => {
const response = (error as any).response;
store.dispatch(actions.restCallFailed(response));
store.dispatch(openDialog({
content: response?.data?.message ?? 'Unknown Error',
content: `${response?.data?.message ?? 'Unknown Error'}.`,
variant: 'error',
}));
}

View File

@ -1,4 +1,5 @@
import { createSelector, createSlice } from '@reduxjs/toolkit';
import events from '../services/event-service';
import React from 'react';
const slice = createSlice({
@ -34,12 +35,6 @@ const slice = createSlice({
toggleSaveChanges: (state, { payload }) => {
state.saveChangesOpen = payload;
},
openedDialog: (state, { payload }: Store.Action<Store.AppState['ui']['openDialog']>) => {
state.openDialog = payload;
},
closedDialog: (state) => {
delete state.openDialog;
},
},
});
@ -67,13 +62,13 @@ export const angrySaveChanges = () => {
saveChanges.style.backgroundColor = 'var(--danger)';
}
// FIXME: crashes app
export const openDialog = (dialog: Store.AppState['ui']['openDialog']) => (dispatch, getState: () => Store.AppState) => {
return;
if (getState().ui.openDialog)
dispatch(actions.closedDialog());
dispatch(actions.openedDialog(dialog));
export const openDialog = (dialog: Dialog) => (dispatch, getState: () => Store.AppState) => {
events.emit('dialog', dialog);
}
export interface Dialog {
content: string | JSX.Element;
variant: 'default' | 'info' | 'error' | 'success' | 'warning';
}
export const closeModal = (dispatch) => {

4
types/store.d.ts vendored
View File

@ -40,10 +40,6 @@ declare namespace Store {
activeGuild?: Entity.Guild;
activeInvite?: Entity.Invite;
activeUser?: Entity.User;
openDialog?: {
content: string | JSX.Element;
variant: 'default' | 'info' | 'error' | 'success' | 'warning';
}
editingMessageId?: string;
saveChangesOpen?: boolean;
};