Improved button styling

Added toast when collection permissions are saved
Removed usage of setState (old habits die hard)
This commit is contained in:
Tom Moor
2019-01-05 18:23:57 -08:00
parent 713473b7d2
commit f80e4ab04c
11 changed files with 131 additions and 112 deletions

View File

@ -1,60 +1,53 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { darken } from 'polished'; import { darken, lighten } from 'polished';
const RealButton = styled.button` const RealButton = styled.button`
display: inline-block; display: inline-block;
margin: 0; margin: 0;
padding: 0; padding: 0;
border: 0; border: 0;
background: ${props => props.theme.primary}; background: ${props => props.theme.blackLight};
color: ${props => props.theme.white}; color: ${props => props.theme.white};
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
border-radius: 4px; border-radius: 4px;
font-size: 15px; font-size: 12px;
font-weight: 500;
height: 36px; height: 36px;
text-decoration: none; text-decoration: none;
text-transform: uppercase;
flex-shrink: 0; flex-shrink: 0;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
user-select: none;
&::-moz-focus-inner { &::-moz-focus-inner {
padding: 0; padding: 0;
border: 0; border: 0;
} }
&:hover {
background: ${props => darken(0.05, props.theme.primary)};
}
svg { &:hover {
position: relative; background: ${props => darken(0.05, props.theme.blackLight)};
top: 0.05em;
} }
&:disabled { &:disabled {
opacity: 0.6;
cursor: default; cursor: default;
pointer-events: none;
color: ${props => lighten(0.2, props.theme.blackLight)};
} }
${props => ${props =>
props.light &&
`
color: ${props.theme.slate};
background: transparent;
border: 1px solid ${props.theme.slate};
&:hover {
background: transparent;
color: ${props.theme.slateDark};
border: 1px solid ${props.theme.slateDark};
}
`} ${props =>
props.neutral && props.neutral &&
` `
background: ${props.theme.slate}; background: ${props.theme.white};
color: ${props.theme.text};
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px;
border: 1px solid ${props.theme.slateLight};
&:hover { &:hover {
background: ${darken(0.05, props.theme.slate)}; background: ${darken(0.05, props.theme.white)};
border: 1px solid ${darken(0.05, props.theme.slateLight)};
} }
`} ${props => `} ${props =>
props.danger && props.danger &&
@ -72,7 +65,7 @@ const Label = styled.span`
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
${props => props.hasIcon && 'padding-left: 2px;'}; ${props => props.hasIcon && 'padding-left: 4px;'};
`; `;
const Inner = styled.span` const Inner = styled.span`
@ -84,7 +77,7 @@ const Inner = styled.span`
${props => ${props =>
props.hasIcon && props.hasIcon &&
(props.small ? 'padding-left: 6px;' : 'padding-left: 10px;')}; (props.small ? 'padding-left: 6px;' : 'padding-left: 8px;')};
`; `;
export type Props = { export type Props = {

View File

@ -25,16 +25,15 @@ type Props = {
@observer @observer
class Collections extends React.Component<Props> { class Collections extends React.Component<Props> {
isPreloaded: boolean = !!this.props.collections.orderedData.length;
componentDidMount() { componentDidMount() {
this.props.collections.fetchPage({ limit: 100 }); this.props.collections.fetchPage({ limit: 100 });
} }
render() { render() {
const { history, location, collections, ui, documents } = this.props; const { history, location, collections, ui, documents } = this.props;
const content = (
return (
collections.isLoaded && (
<Fade>
<Flex column> <Flex column>
<Header>Collections</Header> <Header>Collections</Header>
{collections.orderedData.map(collection => ( {collections.orderedData.map(collection => (
@ -55,8 +54,11 @@ class Collections extends React.Component<Props> {
New collection New collection
</SidebarLink> </SidebarLink>
</Flex> </Flex>
</Fade> );
)
return (
collections.isLoaded &&
(this.isPreloaded ? content : <Fade>{content}</Fade>)
); );
} }
} }

View File

@ -1,5 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components'; import styled from 'styled-components';
import { CloseIcon } from 'outline-icons'; import { CloseIcon } from 'outline-icons';
import Button from './Button'; import Button from './Button';
@ -12,14 +14,10 @@ type Props = {
disabled?: boolean, disabled?: boolean,
}; };
type State = { @observer
isHidden: boolean, class Tip extends React.Component<Props> {
}; @observable
isHidden: boolean = window.localStorage.getItem(this.storageId) === 'hidden';
class Tip extends React.Component<Props, State> {
state = {
isHidden: window.localStorage.getItem(this.storageId) === 'hidden',
};
get storageId() { get storageId() {
return `tip-${this.props.id}`; return `tip-${this.props.id}`;
@ -27,28 +25,29 @@ class Tip extends React.Component<Props, State> {
hide = () => { hide = () => {
window.localStorage.setItem(this.storageId, 'hidden'); window.localStorage.setItem(this.storageId, 'hidden');
this.setState({ isHidden: true }); this.isHidden = true;
}; };
render() { render() {
const { children } = this.props; const { children } = this.props;
if (this.props.disabled || this.state.isHidden) return null; if (this.props.disabled || this.isHidden) return null;
return ( return (
<Wrapper align="center"> <Wrapper align="flex-start">
<span>{children}</span> <span>{children}</span>
<Tooltip tooltip="Hide this message" placement="bottom"> <Tooltip tooltip="Hide this message" placement="bottom">
<Button <Close type="close" size={32} color="#000" onClick={this.hide} />
onClick={this.hide}
icon={<CloseIcon type="close" size={32} color="#FFF" />}
/>
</Tooltip> </Tooltip>
</Wrapper> </Wrapper>
); );
} }
} }
const Close = styled(CloseIcon)`
margin-top: 8px;
`;
const Wrapper = styled(Flex)` const Wrapper = styled(Flex)`
background: ${props => props.theme.primary}; background: ${props => props.theme.primary};
color: ${props => props.theme.text}; color: ${props => props.theme.text};

View File

@ -1,5 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components'; import styled from 'styled-components';
import Tip from './Tip'; import Tip from './Tip';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
@ -9,17 +11,12 @@ type Props = {
team: Team, team: Team,
}; };
type State = { @observer
linkCopied: boolean, class TipInvite extends React.Component<Props> {
}; @observable linkCopied: boolean = false;
class TipInvite extends React.Component<Props, State> {
state = {
linkCopied: false,
};
handleCopy = () => { handleCopy = () => {
this.setState({ linkCopied: true }); this.linkCopied = true;
}; };
render() { render() {
@ -35,7 +32,7 @@ class TipInvite extends React.Component<Props, State> {
{' '} {' '}
<CopyToClipboard text={team.url} onCopy={this.handleCopy}> <CopyToClipboard text={team.url} onCopy={this.handleCopy}>
<a> <a>
{this.state.linkCopied {this.linkCopied
? 'link copied to clipboard!' ? 'link copied to clipboard!'
: 'copy a link to share.'} : 'copy a link to share.'}
</a> </a>

View File

@ -1,5 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components'; import styled from 'styled-components';
type Props = { type Props = {
@ -10,14 +12,10 @@ type Props = {
height?: string, height?: string,
}; };
type State = { @observer
isLoaded: boolean, class Frame extends React.Component<Props> {
};
class Frame extends React.Component<Props, State> {
mounted: boolean; mounted: boolean;
@observable isLoaded: boolean = false;
state = { isLoaded: false };
componentDidMount() { componentDidMount() {
this.mounted = true; this.mounted = true;
@ -30,7 +28,7 @@ class Frame extends React.Component<Props, State> {
loadIframe = () => { loadIframe = () => {
if (!this.mounted) return; if (!this.mounted) return;
this.setState({ isLoaded: true }); this.isLoaded = true;
}; };
render() { render() {
@ -45,7 +43,7 @@ class Frame extends React.Component<Props, State> {
return ( return (
<Rounded width={width} height={height}> <Rounded width={width} height={height}>
{this.state.isLoaded && ( {this.isLoaded && (
<Component <Component
ref={forwardedRef} ref={forwardedRef}
sandbox="allow-same-origin allow-scripts allow-popups allow-forms" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"

View File

@ -142,27 +142,17 @@ class CollectionScene extends React.Component<Props> {
{collection ? ( {collection ? (
<React.Fragment> <React.Fragment>
<PageTitle title={collection.name} /> <PageTitle title={collection.name} />
<Heading>
{collection.private ? (
<PrivateCollectionIcon
color={collection.color}
size={40}
expanded
/>
) : (
<CollectionIcon color={collection.color} size={40} expanded />
)}{' '}
{collection.name}
</Heading>
{collection.isEmpty ? ( {collection.isEmpty ? (
<React.Fragment> <Centered column>
<HelpText> <HelpText>
Collections are for grouping your knowledge base. Get started <strong>{collection.name}</strong> doesnt contain any
by creating a new document. documents yet.<br />Get started by creating a new one!
</HelpText> </HelpText>
<Wrapper> <Wrapper>
<Link to={newDocumentUrl(collection)}> <Link to={newDocumentUrl(collection)}>
<Button>Create new document</Button> <Button icon={<NewDocumentIcon color="#FFF" />}>
Create a document
</Button>
</Link>&nbsp;&nbsp; </Link>&nbsp;&nbsp;
{collection.private && ( {collection.private && (
<Button onClick={this.onPermissions} neutral> <Button onClick={this.onPermissions} neutral>
@ -180,9 +170,26 @@ class CollectionScene extends React.Component<Props> {
onSubmit={this.handlePermissionsModalClose} onSubmit={this.handlePermissionsModalClose}
/> />
</Modal> </Modal>
</React.Fragment> </Centered>
) : ( ) : (
<React.Fragment> <React.Fragment>
<Heading>
{collection.private ? (
<PrivateCollectionIcon
color={collection.color}
size={40}
expanded
/>
) : (
<CollectionIcon
color={collection.color}
size={40}
expanded
/>
)}{' '}
{collection.name}
</Heading>
{collection.description && ( {collection.description && (
<RichMarkdownEditor <RichMarkdownEditor
key={collection.description} key={collection.description}
@ -220,6 +227,13 @@ class CollectionScene extends React.Component<Props> {
} }
} }
const Centered = styled(Flex)`
text-align: center;
margin: 40vh auto 0;
max-width: 380px;
transform: translateY(-50%);
`;
const TinyPinIcon = styled(PinIcon)` const TinyPinIcon = styled(PinIcon)`
position: relative; position: relative;
top: 4px; top: 4px;
@ -227,6 +241,7 @@ const TinyPinIcon = styled(PinIcon)`
`; `;
const Wrapper = styled(Flex)` const Wrapper = styled(Flex)`
justify-content: center;
margin: 10px 0; margin: 10px 0;
`; `;

View File

@ -45,12 +45,12 @@ class CollectionDelete extends React.Component<Props> {
<Flex column> <Flex column>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<HelpText> <HelpText>
Are you sure? Deleting the <strong>{collection.name}</strong>{' '} Are you sure about that? Deleting the{' '}
collection is permanent and will also delete all of the documents <strong>{collection.name}</strong> collection is permanent and will
within it, so be careful with that. also delete all of the documents within it, so be extra careful.
</HelpText> </HelpText>
<Button type="submit" danger> <Button type="submit" danger>
{this.isDeleting ? 'Deleting…' : 'Delete'} {this.isDeleting ? 'Deleting…' : 'Im sure  Delete'}
</Button> </Button>
</form> </form>
</Flex> </Flex>

View File

@ -27,7 +27,8 @@ type Props = {
@observer @observer
class CollectionPermissions extends React.Component<Props> { class CollectionPermissions extends React.Component<Props> {
@observable isSaving: boolean; @observable isEdited: boolean = false;
@observable isSaving: boolean = false;
@observable filter: string; @observable filter: string;
componentDidMount() { componentDidMount() {
@ -35,10 +36,17 @@ class CollectionPermissions extends React.Component<Props> {
this.props.collection.fetchUsers(); this.props.collection.fetchUsers();
} }
componentWillUnmount() {
if (this.isEdited) {
this.props.ui.showToast('Permissions updated');
}
}
handlePrivateChange = async (ev: SyntheticInputEvent<*>) => { handlePrivateChange = async (ev: SyntheticInputEvent<*>) => {
const { collection } = this.props; const { collection } = this.props;
try { try {
this.isEdited = true;
collection.private = ev.target.checked; collection.private = ev.target.checked;
await collection.save(); await collection.save();
@ -53,6 +61,7 @@ class CollectionPermissions extends React.Component<Props> {
handleAddUser = user => { handleAddUser = user => {
try { try {
this.isEdited = true;
this.props.collection.addUser(user); this.props.collection.addUser(user);
} catch (err) { } catch (err) {
this.props.ui.showToast('Could not add user'); this.props.ui.showToast('Could not add user');
@ -61,6 +70,7 @@ class CollectionPermissions extends React.Component<Props> {
handleRemoveUser = user => { handleRemoveUser = user => {
try { try {
this.isEdited = true;
this.props.collection.removeUser(user); this.props.collection.removeUser(user);
} catch (err) { } catch (err) {
this.props.ui.showToast('Could not remove user'); this.props.ui.showToast('Could not remove user');

View File

@ -18,7 +18,7 @@ const UserListItem = ({ user, onAdd, showAdd }: Props) => {
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar src={user.avatarUrl} size={32} />}
actions={ actions={
showAdd ? ( showAdd ? (
<Button type="button" onClick={onAdd}> <Button type="button" onClick={onAdd} neutral>
Invite Invite
</Button> </Button>
) : ( ) : (

View File

@ -45,11 +45,12 @@ class DocumentDelete extends React.Component<Props> {
<Flex column> <Flex column>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<HelpText> <HelpText>
Are you sure? Deleting the <strong>{document.title}</strong>{' '} Are you sure about that? Deleting the{' '}
document is permanent and will also delete all of its history. <strong>{document.title}</strong> document is permanent, will delete
all of its history, and any child documents.
</HelpText> </HelpText>
<Button type="submit" danger> <Button type="submit" danger>
{this.isDeleting ? 'Deleting…' : 'Delete'} {this.isDeleting ? 'Deleting…' : 'Im sure  Delete'}
</Button> </Button>
</form> </form>
</Flex> </Flex>

View File

@ -17,7 +17,11 @@ function SlackButton({ state, scopes, redirectUri, label }: Props) {
(window.location.href = slackAuth(state, scopes, redirectUri)); (window.location.href = slackAuth(state, scopes, redirectUri));
return ( return (
<Button onClick={handleClick} icon={<SpacedSlackLogo size={24} />} neutral> <Button
onClick={handleClick}
icon={<SpacedSlackLogo size={24} fill="#000" />}
neutral
>
{label ? ( {label ? (
label label
) : ( ) : (