Toast type (success/warning/etc)

This commit is contained in:
Tom Moor
2018-05-31 12:07:49 -07:00
parent f633f63a61
commit fb7a8f0312
11 changed files with 58 additions and 36 deletions

View File

@ -112,7 +112,7 @@ class Layout extends React.Component<Props> {
</Content> </Content>
</Flex> </Flex>
<Modals ui={ui} /> <Modals ui={ui} />
<Toasts /> <Toasts ui={ui} />
</Container> </Container>
); );
} }

View File

@ -1,14 +1,18 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { inject, observer } from 'mobx-react'; import { observer } from 'mobx-react';
import styled from 'styled-components'; import styled from 'styled-components';
import { layout } from 'shared/styles/constants'; import { layout } from 'shared/styles/constants';
import Toast from './components/Toast'; import Toast from './components/Toast';
import UiStore from '../../stores/UiStore';
type Props = {
ui: UiStore,
};
@observer @observer
class Toasts extends React.Component<*> { class Toasts extends React.Component<Props> {
handleClose = index => { handleClose = (index: number) => {
this.props.ui.remove(index); this.props.ui.removeToast(index);
}; };
render() { render() {
@ -16,11 +20,11 @@ class Toasts extends React.Component<*> {
return ( return (
<List> <List>
{ui.toasts.map((error, index) => ( {ui.toasts.map((toast, index) => (
<Toast <Toast
key={index} key={index}
onRequestClose={this.handleClose.bind(this, index)} onRequestClose={this.handleClose.bind(this, index)}
message={error} toast={toast}
/> />
))} ))}
</List> </List>
@ -35,6 +39,7 @@ const List = styled.ol`
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
z-index: 1000;
`; `;
export default inject('ui')(Toasts); export default Toasts;

View File

@ -4,12 +4,12 @@ import styled from 'styled-components';
import { darken } from 'polished'; import { darken } from 'polished';
import { color } from 'shared/styles/constants'; import { color } from 'shared/styles/constants';
import { fadeAndScaleIn } from 'shared/styles/animations'; import { fadeAndScaleIn } from 'shared/styles/animations';
import type { Toast as TToast } from '../../../types';
type Props = { type Props = {
onRequestClose: () => void, onRequestClose: () => void,
closeAfterMs: number, closeAfterMs: number,
message: string, toast: TToast,
type: 'warning' | 'error' | 'info',
}; };
class Toast extends React.Component<Props> { class Toast extends React.Component<Props> {
@ -17,7 +17,6 @@ class Toast extends React.Component<Props> {
static defaultProps = { static defaultProps = {
closeAfterMs: 3000, closeAfterMs: 3000,
type: 'warning',
}; };
componentDidMount() { componentDidMount() {
@ -32,14 +31,14 @@ class Toast extends React.Component<Props> {
} }
render() { render() {
const { type, onRequestClose } = this.props; const { toast, onRequestClose } = this.props;
const message = const message =
typeof this.props.message === 'string' typeof toast.message === 'string'
? this.props.message ? toast.message
: this.props.message.toString(); : toast.message.toString();
return ( return (
<Container onClick={onRequestClose} type={type}> <Container onClick={onRequestClose} type={toast.type}>
<Message>{message}</Message> <Message>{message}</Message>
</Container> </Container>
); );

View File

@ -4,7 +4,7 @@ import { withRouter } from 'react-router-dom';
import { inject } from 'mobx-react'; import { inject } from 'mobx-react';
import { MoreIcon } from 'outline-icons'; import { MoreIcon } from 'outline-icons';
import { Share } from 'types'; import type { Share } from 'types';
import CopyToClipboard from 'components/CopyToClipboard'; import CopyToClipboard from 'components/CopyToClipboard';
import SharesStore from 'stores/SharesStore'; import SharesStore from 'stores/SharesStore';
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu'; import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';

View File

@ -6,6 +6,7 @@ import styled from 'styled-components';
import { color, size } from 'shared/styles/constants'; import { color, size } from 'shared/styles/constants';
import AuthStore from 'stores/AuthStore'; import AuthStore from 'stores/AuthStore';
import UiStore from 'stores/UiStore';
import ImageUpload from './components/ImageUpload'; import ImageUpload from './components/ImageUpload';
import Input, { LabelText } from 'components/Input'; import Input, { LabelText } from 'components/Input';
import Button from 'components/Button'; import Button from 'components/Button';
@ -15,6 +16,7 @@ import Flex from 'shared/components/Flex';
type Props = { type Props = {
auth: AuthStore, auth: AuthStore,
ui: UiStore,
}; };
@observer @observer
@ -41,6 +43,7 @@ class Profile extends React.Component<Props> {
name: this.name, name: this.name,
avatarUrl: this.avatarUrl, avatarUrl: this.avatarUrl,
}); });
this.props.ui.showToast('Profile saved', 'success');
}; };
handleNameChange = (ev: SyntheticInputEvent<*>) => { handleNameChange = (ev: SyntheticInputEvent<*>) => {
@ -56,7 +59,7 @@ class Profile extends React.Component<Props> {
}; };
render() { render() {
const { user } = this.props.auth; const { user, isSaving } = this.props.auth;
if (!user) return null; if (!user) return null;
const avatarUrl = this.avatarUrl || user.avatarUrl; const avatarUrl = this.avatarUrl || user.avatarUrl;
@ -73,7 +76,7 @@ class Profile extends React.Component<Props> {
> >
<Avatar src={avatarUrl} /> <Avatar src={avatarUrl} />
<Flex auto align="center" justify="center"> <Flex auto align="center" justify="center">
Upload new image Upload
</Flex> </Flex>
</ImageUpload> </ImageUpload>
</AvatarContainer> </AvatarContainer>
@ -85,8 +88,8 @@ class Profile extends React.Component<Props> {
onChange={this.handleNameChange} onChange={this.handleNameChange}
required required
/> />
<Button type="submit" disabled={this.isSaving || !this.name}> <Button type="submit" disabled={isSaving || !this.name}>
Save {isSaving ? 'Saving…' : 'Save'}
</Button> </Button>
</form> </form>
</CenteredContent> </CenteredContent>
@ -101,7 +104,7 @@ const ProfilePicture = styled(Flex)`
const avatarStyles = ` const avatarStyles = `
width: 80px; width: 80px;
height: 80px; height: 80px;
border-radius: 10px; border-radius: 50%;
`; `;
const AvatarContainer = styled(Flex)` const AvatarContainer = styled(Flex)`

View File

@ -11,7 +11,7 @@ type Props = {
auth: AuthStore, auth: AuthStore,
scopes?: string[], scopes?: string[],
redirectUri?: string, redirectUri?: string,
state?: string, state: string,
label?: string, label?: string,
}; };

View File

@ -12,6 +12,7 @@ class AuthStore {
@observable user: ?User; @observable user: ?User;
@observable team: ?Team; @observable team: ?Team;
@observable token: ?string; @observable token: ?string;
@observable isSaving: boolean = false;
@observable isLoading: boolean = false; @observable isLoading: boolean = false;
@observable isSuspended: boolean = false; @observable isSuspended: boolean = false;
@observable suspendedContactEmail: ?string; @observable suspendedContactEmail: ?string;
@ -50,13 +51,19 @@ class AuthStore {
}; };
@action @action
updateUser = async (params: { name: string, avatarUrl?: string }) => { updateUser = async (params: { name: string, avatarUrl: ?string }) => {
const res = await client.post(`/user.update`, params); this.isSaving = true;
invariant(res && res.data, 'User response not available');
runInAction('AuthStore#updateUser', () => { try {
this.user = res.data.user; const res = await client.post(`/user.update`, params);
}); invariant(res && res.data, 'User response not available');
runInAction('AuthStore#updateUser', () => {
this.user = res.data;
});
} finally {
this.isSaving = false;
}
}; };
@action @action

View File

@ -4,7 +4,6 @@ import { client } from 'utils/ApiClient';
import _ from 'lodash'; import _ from 'lodash';
import invariant from 'invariant'; import invariant from 'invariant';
import stores from 'stores';
import BaseStore from './BaseStore'; import BaseStore from './BaseStore';
import UiStore from './UiStore'; import UiStore from './UiStore';
import Collection from 'models/Collection'; import Collection from 'models/Collection';

View File

@ -2,6 +2,7 @@
import { observable, action } from 'mobx'; import { observable, action } from 'mobx';
import Document from 'models/Document'; import Document from 'models/Document';
import Collection from 'models/Collection'; import Collection from 'models/Collection';
import type { Toast } from '../types';
class UiStore { class UiStore {
@observable activeModalName: ?string; @observable activeModalName: ?string;
@ -11,7 +12,7 @@ class UiStore {
@observable progressBarVisible: boolean = false; @observable progressBarVisible: boolean = false;
@observable editMode: boolean = false; @observable editMode: boolean = false;
@observable mobileSidebarVisible: boolean = false; @observable mobileSidebarVisible: boolean = false;
@observable toasts: string[] = observable.array([]); @observable toasts: Toast[] = observable.array([]);
/* Actions */ /* Actions */
@action @action
@ -82,8 +83,11 @@ class UiStore {
} }
@action @action
showToast = (message: string): void => { showToast = (
this.toasts.push(message); message: string,
type?: 'warning' | 'error' | 'info' | 'success' = 'warning'
): void => {
this.toasts.push({ message, type });
}; };
@action @action

View File

@ -9,6 +9,11 @@ export type User = {
isSuspended?: boolean, isSuspended?: boolean,
}; };
export type Toast = {
message: string,
type: 'warning' | 'error' | 'info' | 'success',
};
export type Share = { export type Share = {
id: string, id: string,
url: string, url: string,

View File

@ -47,9 +47,9 @@ export const color = {
/* Brand */ /* Brand */
primary: '#1AB6FF', primary: '#1AB6FF',
danger: '#D0021B', danger: '#D0021B',
warning: '#f08a24' /* replace */, warning: '#f08a24',
success: '#43AC6A' /* replace */, success: '#1AB6FF',
info: '#a0d3e8' /* replace */, info: '#a0d3e8',
offline: '#000000', offline: '#000000',
/* Dark Grays */ /* Dark Grays */