Merge branch 'master' into keyboard-shortcuts-modal

This commit is contained in:
Jori Lallo
2017-07-12 23:06:36 -07:00
committed by GitHub
18 changed files with 254 additions and 148 deletions

View File

@@ -1,10 +1,8 @@
{
"presets": [
"react",
"env"
],
"presets": ["react", "env"],
"plugins": [
"lodash",
"styled-components",
"transform-decorators-legacy",
"transform-es2015-destructuring",
"transform-object-rest-spread",
@@ -13,9 +11,7 @@
],
"env": {
"development": {
"presets": [
"react-hmre"
]
"presets": ["react-hmre"]
}
}
}
}

View File

@@ -1,66 +1,111 @@
// @flow
import React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import Flex from 'components/Flex';
import { color } from 'styles/constants';
import styles from './DropdownMenu.scss';
const MenuItem = ({
onClick,
children,
}: {
type MenuItemProps = {
onClick?: Function,
children?: React.Element<any>,
}) => {
};
const DropdownMenuItem = ({ onClick, children }: MenuItemProps) => {
return (
<div className={styles.menuItem} onClick={onClick}>
<MenuItem onClick={onClick}>
{children}
</div>
</MenuItem>
);
};
//
type DropdownMenuProps = {
label: React.Element<any>,
children?: React.Element<any>,
};
class DropdownMenu extends React.Component {
static propTypes = {
label: React.PropTypes.node.isRequired,
children: React.PropTypes.node.isRequired,
};
@observer class DropdownMenu extends React.Component {
props: DropdownMenuProps;
@observable menuOpen: boolean = false;
state = {
menuVisible: false,
};
onMouseEnter = () => {
this.setState({ menuVisible: true });
};
onMouseLeave = () => {
this.setState({ menuVisible: false });
};
onClick = () => {
this.setState({ menuVisible: !this.state.menuVisible });
handleClick = () => {
this.menuOpen = !this.menuOpen;
};
render() {
return (
<div
className={styles.menuContainer}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div className={styles.label} onClick={this.onClick}>
{this.props.label}
</div>
<MenuContainer onClick={this.handleClick}>
{this.menuOpen && <Backdrop />}
{this.state.menuVisible
? <div className={styles.menu}>
{this.props.children}
</div>
: null}
</div>
<Label>
{this.props.label}
</Label>
{this.menuOpen &&
<Menu>
{this.props.children}
</Menu>}
</MenuContainer>
);
}
}
export default DropdownMenu;
export { MenuItem };
const Backdrop = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
`;
const Label = styled(Flex).attrs({
justify: 'center',
align: 'center',
})`
cursor: pointer;
z-index: 1000;
min-height: 43px;
margin: 0 5px;
`;
const MenuContainer = styled.div`
position: relative;
`;
const Menu = styled.div`
position: absolute;
right: 0;
z-index: 1000;
border: 1px solid #eee;
background-color: #fff;
min-width: 160px;
`;
const MenuItem = styled.div`
margin: 0;
padding: 5px 10px;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-left: 2px solid transparent;
span {
margin-top: 2px;
}
a {
text-decoration: none;
width: 100%;
}
&:hover {
border-left: 2px solid ${color.primary};
}
`;
export { DropdownMenu, DropdownMenuItem };

View File

@@ -1,52 +0,0 @@
@import '~styles/constants.scss';
.label {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
min-height: 43px;
margin: 0 5px;
color: $actionColor;
}
.menuContainer {
position: relative;
.menu {
position: absolute;
top: $headerHeight;
right: 0;
z-index: 1000;
border: 1px solid #eee;
background-color: #fff;
min-width: 160px;
}
}
.menuItem {
margin: 0;
padding: 5px 10px;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-left: 2px solid transparent;
span {
margin-top: 2px;
}
a {
color: $textColor;
text-decoration: none;
width: 100%;
}
&:hover {
border-left: 2px solid $actionColor;
}
}

View File

@@ -1,5 +1,4 @@
// @flow
import DropdownMenu, { MenuItem } from './DropdownMenu';
import { DropdownMenu, DropdownMenuItem } from './DropdownMenu';
import MoreIcon from './components/MoreIcon';
export default DropdownMenu;
export { MenuItem, MoreIcon };
export { DropdownMenu, DropdownMenuItem, MoreIcon };

View File

@@ -89,11 +89,12 @@ type KeyData = {
case 's':
ev.preventDefault();
ev.stopPropagation();
return this.props.onSave({ redirect: false });
this.props.onSave();
return state;
case 'enter':
ev.preventDefault();
ev.stopPropagation();
this.props.onSave();
this.props.onSave({ redirect: false });
return state;
case 'escape':
return this.props.onCancel();

View File

@@ -0,0 +1,21 @@
// @flow
import React from 'react';
import Icon from './Icon';
import type { Props } from './Icon';
export default function CheckIcon(props: Props) {
return (
<Icon {...props}>
<svg
fill="#000000"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
</Icon>
);
}

View File

@@ -9,7 +9,7 @@ import keydown from 'react-keydown';
import Flex from 'components/Flex';
import { color, layout } from 'styles/constants';
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
import Scrollable from 'components/Scrollable';
import KeyboardShortcuts from 'components/KeyboardShortcuts';
@@ -107,15 +107,17 @@ type Props = {
</Flex>
<DropdownMenu label={<Avatar src={user.user.avatarUrl} />}>
<MenuLink to="/settings">
<MenuItem>Settings</MenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
</MenuLink>
<MenuItem onClick={this.handleOpenKeyboardShortcuts}>
Keyboard shortcuts
</MenuItem>
<MenuLink to="/developers">
<MenuItem>API</MenuItem>
<DropdownMenuItem>API</DropdownMenuItem>
</MenuLink>
<MenuItem onClick={this.handleLogout}>Logout</MenuItem>
<DropdownMenuItem onClick={this.handleLogout}>
Logout
</DropdownMenuItem>
</DropdownMenu>
</Header>

View File

@@ -1,14 +1,17 @@
// @flow
import React from 'react';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import CheckIcon from 'components/Icon/CheckIcon';
import { fadeAndScaleIn } from 'styles/animations';
type Props = {
onClick: Function,
showCheckmark: boolean,
disabled?: boolean,
isNew?: boolean,
};
@observer class SaveAction extends React.Component {
class SaveAction extends React.Component {
props: Props;
onClick = (event: MouseEvent) => {
@@ -19,21 +22,38 @@ type Props = {
};
render() {
const { disabled, isNew } = this.props;
const { showCheckmark, disabled, isNew } = this.props;
return (
<div>
<a
href
onClick={this.onClick}
style={{ opacity: disabled ? 0.5 : 1 }}
title="Save changes (Cmd+Enter)"
>
{isNew ? 'Publish' : 'Save'}
</a>
</div>
<Link
href
onClick={this.onClick}
style={{ opacity: disabled ? 0.5 : 1 }}
title="Save changes (Cmd+Enter)"
>
{showCheckmark && <SavedIcon />}
{isNew ? 'Publish' : 'Save'}
</Link>
);
}
}
const Link = styled.a`
display: flex;
align-items: center;
`;
const SavedIcon = styled(CheckIcon)`
animation: ${fadeAndScaleIn} 250ms ease;
display: inline-block;
margin-right: 4px;
width: 18px;
height: 18px;
svg {
width: 18px;
height: 18px;
}
`;
export default SaveAction;

View File

@@ -2,7 +2,7 @@
import React from 'react';
import styled from 'styled-components';
import ReactModal from 'react-modal';
import { modalFadeIn } from 'styles/animations';
import { fadeAndScaleIn } from 'styles/animations';
import CloseIcon from 'components/Icon/CloseIcon';
import Flex from 'components/Flex';
@@ -46,7 +46,7 @@ const Content = styled(Flex)`
`;
const StyledModal = styled(ReactModal)`
animation: ${modalFadeIn} 250ms ease;
animation: ${fadeAndScaleIn} 250ms ease;
position: absolute;
top: 0;

View File

@@ -37,6 +37,7 @@ type Props = {
@observer class DocumentScene extends Component {
props: Props;
savedTimeout: number;
state: {
newDocument?: Document,
};
@@ -44,6 +45,7 @@ type Props = {
isDragging: false,
isLoading: false,
newDocument: undefined,
showAsSaved: false,
};
componentDidMount() {
@@ -60,6 +62,7 @@ type Props = {
}
componentWillUnmount() {
clearTimeout(this.savedTimeout);
this.props.ui.clearActiveDocument();
}
@@ -108,9 +111,19 @@ type Props = {
if (redirect || this.props.newDocument) {
this.props.history.push(document.url);
} else {
this.showAsSaved();
}
};
showAsSaved() {
this.setState({ showAsSaved: true });
this.savedTimeout = setTimeout(
() => this.setState({ showAsSaved: false }),
2000
);
}
onImageUploadStart() {
this.setState({ isLoading: true });
}
@@ -204,6 +217,7 @@ type Props = {
<HeaderAction>
{isEditing
? <SaveAction
showCheckmark={this.state.showAsSaved}
onClick={this.onSave.bind(this, true)}
disabled={get(this.document, 'isSaving')}
isNew={!!isNew}

View File

@@ -5,7 +5,11 @@ import get from 'lodash/get';
import { withRouter } from 'react-router-dom';
import { observer } from 'mobx-react';
import Document from 'models/Document';
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
import {
DropdownMenu,
DropdownMenuItem,
MoreIcon,
} from 'components/DropdownMenu';
type Props = {
history: Object,
@@ -63,12 +67,13 @@ type Props = {
<DropdownMenu label={<MoreIcon />}>
{collection &&
<div>
<MenuItem onClick={this.onCreateDocument}>
<DropdownMenuItem onClick={this.onCreateDocument}>
New document
</MenuItem>
</DropdownMenuItem>
</div>}
<MenuItem onClick={this.onExport}>Export</MenuItem>
{allowDelete && <MenuItem onClick={this.onDelete}>Delete</MenuItem>}
<DropdownMenuItem onClick={this.onExport}>Export</DropdownMenuItem>
{allowDelete &&
<DropdownMenuItem onClick={this.onDelete}>Delete</DropdownMenuItem>}
</DropdownMenu>
);
}

View File

@@ -9,10 +9,10 @@ import { searchUrl } from 'utils/routeHelpers';
import styled from 'styled-components';
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
import CenteredContent from 'components/CenteredContent';
import SearchField from './components/SearchField';
import SearchStore from './SearchStore';
import CenteredContent from 'components/CenteredContent';
import DocumentPreview from 'components/DocumentPreview';
import PageTitle from 'components/PageTitle';
@@ -23,7 +23,9 @@ type Props = {
};
const Container = styled(CenteredContent)`
position: relative;
> div {
position: relative;
}
`;
const ResultsWrapper = styled(Flex)`
@@ -39,6 +41,12 @@ const ResultList = styled(Flex)`
transition: all 400ms cubic-bezier(0.65, 0.05, 0.36, 1);
`;
const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
display: flex;
flex-direction: column;
flex: 1;
`;
@observer class Search extends React.Component {
firstDocument: HTMLElement;
props: Props;
@@ -106,7 +114,7 @@ const ResultList = styled(Flex)`
value={query || ''}
/>
<ResultList visible={hasResults}>
<ArrowKeyNavigation
<StyledArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
@@ -118,7 +126,7 @@ const ResultList = styled(Flex)`
highlight={this.store.searchTerm}
/>
))}
</ArrowKeyNavigation>
</StyledArrowKeyNavigation>
</ResultList>
</ResultsWrapper>
</Container>

View File

@@ -1,7 +1,7 @@
// @flow
import { keyframes } from 'styled-components';
export const modalFadeIn = keyframes`
export const fadeAndScaleIn = keyframes`
from {
opacity: 0;
transform: scale(.98);

View File

@@ -64,6 +64,7 @@
"babel-eslint": "^7.2.3",
"babel-loader": "6.2.5",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-styled-components": "^1.1.7",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "1.3.4",
"babel-plugin-transform-es2015-destructuring": "^6.23.0",

View File

@@ -9,6 +9,15 @@ Object {
}
`;
exports[`#documents.search should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;
exports[`#documents.star should require authentication 1`] = `
Object {
"error": "authentication_required",

View File

@@ -41,6 +41,28 @@ describe('#documents.list', async () => {
});
});
describe('#documents.search', async () => {
it('should return results', async () => {
const { user } = await seed();
const res = await server.post('/api/documents.search', {
body: { token: user.getJwtToken(), query: 'much' },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.length).toEqual(1);
expect(body.data[0].text).toEqual('# Much guidance');
});
it('should require authentication', async () => {
const res = await server.post('/api/documents.search');
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
});
describe('#documents.viewed', async () => {
it('should return empty result if no views', async () => {
const { user } = await seed();

View File

@@ -156,7 +156,7 @@ const Document = sequelize.define(
});
}
},
searchForUser: (user, query, options = {}) => {
searchForUser: async (user, query, options = {}) => {
const limit = options.limit || 15;
const offset = options.offset || 0;
@@ -169,13 +169,18 @@ const Document = sequelize.define(
LIMIT :limit OFFSET :offset;
`;
return sequelize.query(sql, {
replacements: {
query,
limit,
offset,
},
model: Document,
const ids = await sequelize
.query(sql, {
replacements: {
query,
limit,
offset,
},
model: Document,
})
.map(document => document.id);
return Document.findAll({
where: { id: ids },
});
},
},

View File

@@ -534,6 +534,12 @@ babel-plugin-react-transform@^2.0.2:
dependencies:
lodash "^4.6.1"
babel-plugin-styled-components@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.1.7.tgz#a92c239779cc80e7838b645c12865c61c4ca71ce"
dependencies:
stylis "^3.2.1"
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
@@ -8259,6 +8265,10 @@ stylis@^2.0.0:
version "2.0.12"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-2.0.12.tgz#547253055d170f2a7ac2f6d09365d70635f2bec6"
stylis@^3.2.1:
version "3.2.3"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.2.3.tgz#fed751d792af3f48a247769f55aca05c1a100a09"
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -8913,7 +8923,7 @@ whatwg-encoding@^1.0.1:
dependencies:
iconv-lite "0.4.13"
whatwg-fetch@1.0.0, whatwg-fetch@>=0.10.0:
whatwg-fetch@>=0.10.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-1.0.0.tgz#01c2ac4df40e236aaa18480e3be74bd5c8eb798e"