Add REST Error Dialogs
This commit is contained in:
parent
0a542c5829
commit
47e0f99253
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
@ -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">
|
||||
|
@ -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 />
|
||||
|
16
frontend/src/components/utils/input/three-toggle.scoped.css
Normal file
16
frontend/src/components/utils/input/three-toggle.scoped.css
Normal 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%);
|
||||
}
|
@ -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" />
|
||||
|
@ -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%);
|
||||
}
|
||||
|
@ -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;
|
@ -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
|
||||
|
5
frontend/src/services/event-service.ts
Normal file
5
frontend/src/services/event-service.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const events = new EventEmitter();
|
||||
|
||||
export default events;
|
@ -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',
|
||||
}));
|
||||
}
|
||||
|
@ -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
4
types/store.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user