Merge master, small refactor
This commit is contained in:
@@ -20,6 +20,9 @@ SLACK_SECRET=d2dc414f9953226bad0a356cXXXXYYYY
|
||||
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
# Comma separated list of domains to be allowed (optional)
|
||||
# If not set, all Google apps domains are allowed by default
|
||||
GOOGLE_ALLOWED_DOMAINS=
|
||||
|
||||
# Third party credentials (optional)
|
||||
SLACK_VERIFICATION_TOKEN=PLxk6OlXXXXXVj3YYYY
|
||||
|
||||
@@ -8,3 +8,4 @@ COPY . $APP_PATH
|
||||
RUN yarn
|
||||
RUN cp -r /opt/outline/node_modules /opt/node_modules
|
||||
|
||||
CMD yarn build && yarn start
|
||||
@@ -10,6 +10,7 @@
|
||||
<img src="https://circleci.com/gh/outline/outline.svg?style=shield&circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a" alt="" data-canonical-src="" style="max-width:100%;">
|
||||
<a href="https://spectrum.chat/outline" rel="nofollow"><img src="https://withspectrum.github.io/badge/badge.svg" alt="Join the community on Spectrum"/></a>
|
||||
<a href="https://github.com/prettier/prettier"><img src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat"></a>
|
||||
<a href="https://github.com/styled-components/styled-components"><img src="https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg"></a>
|
||||
</p>
|
||||
|
||||
This is the source code that runs [**Outline**](https://www.getoutline.com) and all the associated services. If you want to use Outline then you don't need to run this code, we offer a hosted version of the app at [getoutline.com](https://www.getoutline.com).
|
||||
@@ -106,7 +107,7 @@ yarn test:app
|
||||
|
||||
Outline is still built and maintained by a small team – we'd love your help to fix bugs and add features!
|
||||
|
||||
However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in our [Spectrum community](https://spectrum.chat/outline). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster!
|
||||
However, before working on a pull request please let the core team know by creating or commenting in an issue on [GitHub](https://www.github.com/outline/outline/issues), and we'd also love to hear from you in our [Spectrum community](https://spectrum.chat/outline). This way we can ensure that an approach is agreed on before code is written and will hopefully help to get your contributions integrated faster! Take a look at our [roadmap](https://www.getoutline.com/share/3e6cb2b5-d68b-4ad8-8900-062476820311).
|
||||
|
||||
If you’re looking for ways to get started, here's a list of ways to help us improve Outline:
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { layout, color } from 'shared/styles/constants';
|
||||
|
||||
export const Action = styled(Flex)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 0 0 12px;
|
||||
font-size: 15px;
|
||||
|
||||
a {
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
@@ -19,7 +19,7 @@ export const Separator = styled.div`
|
||||
margin-left: 12px;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: ${color.slateLight};
|
||||
background: ${props => props.theme.slateLight};
|
||||
`;
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
@@ -29,7 +29,7 @@ const Actions = styled(Flex)`
|
||||
left: 0;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
|
||||
@media print {
|
||||
@@ -38,7 +38,7 @@ const Actions = styled(Flex)`
|
||||
|
||||
${breakpoint('tablet')`
|
||||
left: auto;
|
||||
padding: ${layout.vpadding} ${layout.hpadding} 8px 8px;
|
||||
padding: 24px;
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
@@ -31,7 +30,7 @@ const Container = styled(Flex)`
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
|
||||
background-color: ${({ type }) => color[type]};
|
||||
background-color: ${({ theme, type }) => theme.color[type]};
|
||||
`;
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import placeholder from './placeholder.png';
|
||||
|
||||
type Props = {
|
||||
@@ -38,7 +37,7 @@ const CircleImg = styled.img`
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid ${color.white};
|
||||
border: 2px solid ${props => props.theme.white};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { darken } from 'polished';
|
||||
|
||||
const RealButton = styled.button`
|
||||
@@ -9,8 +8,8 @@ const RealButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: ${color.primary};
|
||||
color: ${color.white};
|
||||
background: ${props => props.theme.primary};
|
||||
color: ${props => props.theme.white};
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
height: 36px;
|
||||
@@ -24,7 +23,7 @@ const RealButton = styled.button`
|
||||
border: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.primary)};
|
||||
background: ${props => darken(0.05, props.theme.primary)};
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -40,30 +39,30 @@ const RealButton = styled.button`
|
||||
${props =>
|
||||
props.light &&
|
||||
`
|
||||
color: ${color.slate};
|
||||
color: ${props.theme.slate};
|
||||
background: transparent;
|
||||
border: 1px solid ${color.slate};
|
||||
border: 1px solid ${props.theme.slate};
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
color: ${color.slateDark};
|
||||
border: 1px solid ${color.slateDark};
|
||||
color: ${props.theme.slateDark};
|
||||
border: 1px solid ${props.theme.slateDark};
|
||||
}
|
||||
`} ${props =>
|
||||
props.neutral &&
|
||||
`
|
||||
background: ${color.slate};
|
||||
background: ${props.theme.slate};
|
||||
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.slate)};
|
||||
background: ${darken(0.05, props.theme.slate)};
|
||||
}
|
||||
`} ${props =>
|
||||
props.danger &&
|
||||
`
|
||||
background: ${color.danger};
|
||||
background: ${props.theme.danger};
|
||||
|
||||
&:hover {
|
||||
background: ${darken(0.05, color.danger)};
|
||||
background: ${darken(0.05, props.theme.danger)};
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
10
app/components/ClickablePadding.js
Normal file
10
app/components/ClickablePadding.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ClickablePadding = styled.div`
|
||||
min-height: 50vh;
|
||||
cursor: ${({ onClick }) => (onClick ? 'text' : 'default')};
|
||||
${({ grow }) => grow && `flex-grow: 1;`};
|
||||
`;
|
||||
|
||||
export default ClickablePadding;
|
||||
@@ -31,22 +31,21 @@ const Collaborators = ({ document }: Props) => {
|
||||
|
||||
return (
|
||||
<Avatars>
|
||||
<StyledTooltip tooltip={tooltip} placement="bottom">
|
||||
{collaborators.map(user => (
|
||||
<AvatarWrapper key={user.id}>
|
||||
{collaborators.map(user => (
|
||||
<Tooltip
|
||||
tooltip={collaborators.length > 1 ? user.name : tooltip}
|
||||
placement="bottom"
|
||||
key={user.id}
|
||||
>
|
||||
<AvatarWrapper>
|
||||
<Avatar src={user.avatarUrl} />
|
||||
</AvatarWrapper>
|
||||
))}
|
||||
</StyledTooltip>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatars>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { LabelText, Outline } from 'components/Input';
|
||||
import { color, fonts, fontWeight } from 'shared/styles/constants';
|
||||
import { validateColorHex } from 'shared/utils/color';
|
||||
|
||||
const colors = [
|
||||
@@ -165,7 +164,7 @@ const StyledOutline = styled(Outline)`
|
||||
const HexHash = styled.div`
|
||||
margin-left: 12px;
|
||||
padding-bottom: 0;
|
||||
font-weight: ${fontWeight.medium};
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
@@ -177,13 +176,13 @@ const CustomColorInput = styled.input`
|
||||
padding-bottom: 0;
|
||||
outline: none;
|
||||
background: none;
|
||||
font-family: ${fonts.monospace};
|
||||
font-weight: ${fontWeight.medium};
|
||||
font-family: ${props => props.theme.monospaceFontFamily};
|
||||
font-weight: 500;
|
||||
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
font-family: ${fonts.monospace};
|
||||
font-weight: ${fontWeight.medium};
|
||||
color: ${props => props.theme.slate};
|
||||
font-family: ${props => props.theme.monospaceFontFamily};
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Document from 'models/Document';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Highlight from 'components/Highlight';
|
||||
import { StarredIcon } from 'outline-icons';
|
||||
@@ -15,11 +14,11 @@ type Props = {
|
||||
document: Document,
|
||||
highlight?: ?string,
|
||||
showCollection?: boolean,
|
||||
innerRef?: Function,
|
||||
innerRef?: *,
|
||||
};
|
||||
|
||||
const StyledStar = styled(({ solid, ...props }) => (
|
||||
<StarredIcon color={solid ? color.black : color.text} {...props} />
|
||||
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
|
||||
<StarredIcon color={solid ? theme.black : theme.text} {...props} />
|
||||
))`
|
||||
opacity: ${props => (props.solid ? '1 !important' : 0)};
|
||||
transition: all 100ms ease-in-out;
|
||||
@@ -30,7 +29,7 @@ const StyledStar = styled(({ solid, ...props }) => (
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
`;
|
||||
`);
|
||||
|
||||
const StyledDocumentMenu = styled(DocumentMenu)`
|
||||
position: absolute;
|
||||
@@ -57,8 +56,8 @@ const DocumentLink = styled(Link)`
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: ${color.smokeLight};
|
||||
border: 2px solid ${color.smoke};
|
||||
background: ${props => props.theme.smokeLight};
|
||||
border: 2px solid ${props => props.theme.smoke};
|
||||
outline: none;
|
||||
|
||||
${StyledStar}, ${StyledDocumentMenu} {
|
||||
@@ -71,7 +70,7 @@ const DocumentLink = styled(Link)`
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 2px solid ${color.slateDark};
|
||||
border: 2px solid ${props => props.theme.slateDark};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Time from 'shared/components/Time';
|
||||
|
||||
const Container = styled(Flex)`
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const Modified = styled.span`
|
||||
color: ${props => (props.highlight ? color.slateDark : color.slate)};
|
||||
color: ${props =>
|
||||
props.highlight ? props.theme.slateDark : props.theme.slate};
|
||||
font-weight: ${props => (props.highlight ? '600' : '400')};
|
||||
`;
|
||||
|
||||
@@ -23,46 +23,36 @@ type Props = {
|
||||
views?: number,
|
||||
};
|
||||
|
||||
class PublishingInfo extends React.Component<Props> {
|
||||
render() {
|
||||
const { collection, document } = this.props;
|
||||
const {
|
||||
modifiedSinceViewed,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
publishedAt,
|
||||
} = document;
|
||||
function PublishingInfo({ collection, document }: Props) {
|
||||
const { modifiedSinceViewed, updatedAt, updatedBy, publishedAt } = document;
|
||||
|
||||
const timeAgo = `${distanceInWordsToNow(new Date(createdAt))} ago`;
|
||||
|
||||
return (
|
||||
<Container align="center">
|
||||
{publishedAt === updatedAt ? (
|
||||
<span>
|
||||
{createdBy.name} published {timeAgo}
|
||||
</span>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{updatedBy.name}
|
||||
{publishedAt ? (
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
modified {timeAgo}
|
||||
</Modified>
|
||||
) : (
|
||||
<span> saved {timeAgo}</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{collection && (
|
||||
<span>
|
||||
in <strong>{collection.name}</strong>
|
||||
</span>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container align="center">
|
||||
{publishedAt && publishedAt === updatedAt ? (
|
||||
<span>
|
||||
{updatedBy.name} published <Time dateTime={publishedAt} /> ago
|
||||
</span>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{updatedBy.name}
|
||||
{publishedAt ? (
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
modified <Time dateTime={updatedAt} /> ago
|
||||
</Modified>
|
||||
) : (
|
||||
<span>
|
||||
saved <Time dateTime={updatedAt} /> ago
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{collection && (
|
||||
<span>
|
||||
in <strong>{collection.name}</strong>
|
||||
</span>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublishingInfo;
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { injectGlobal } from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import importFile from 'utils/importFile';
|
||||
import invariant from 'invariant';
|
||||
import _ from 'lodash';
|
||||
@@ -25,12 +24,12 @@ type Props = {
|
||||
// eslint-disable-next-line
|
||||
injectGlobal`
|
||||
.activeDropZone {
|
||||
background: ${color.slateDark};
|
||||
svg { fill: ${color.white}; }
|
||||
background: ${props => props.theme.slateDark};
|
||||
svg { fill: ${props => props.theme.white}; }
|
||||
}
|
||||
|
||||
.activeDropZone a {
|
||||
color: ${color.white} !important;
|
||||
color: ${props => props.theme.white} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { PortalWithState } from 'react-portal';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
|
||||
type Props = {
|
||||
@@ -90,8 +89,8 @@ const Menu = styled.div`
|
||||
right: ${({ right }) => right}px;
|
||||
top: ${({ top }) => top}px;
|
||||
z-index: 1000;
|
||||
border: ${color.slateLight};
|
||||
background: ${color.white};
|
||||
border: ${props => props.theme.slateLight};
|
||||
background: ${props => props.theme.white};
|
||||
border-radius: 2px;
|
||||
padding: 0.5em 0;
|
||||
min-width: 160px;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
onClick?: (SyntheticEvent<*>) => *,
|
||||
children?: React.Node,
|
||||
disabled?: boolean,
|
||||
};
|
||||
|
||||
const DropdownMenuItem = ({ onClick, children, ...rest }: Props) => {
|
||||
@@ -22,24 +22,32 @@ const MenuItem = styled.a`
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
|
||||
color: ${color.slateDark};
|
||||
color: ${props =>
|
||||
props.disabled ? props.theme.slate : props.theme.slateDark};
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
cursor: default;
|
||||
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
${props =>
|
||||
props.disabled
|
||||
? ''
|
||||
: `
|
||||
|
||||
&:hover {
|
||||
color: ${color.white};
|
||||
background: ${color.primary};
|
||||
color: ${props.theme.white};
|
||||
background: ${props.theme.primary};
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
fill: ${color.white};
|
||||
fill: ${props.theme.white};
|
||||
}
|
||||
}
|
||||
`};
|
||||
`;
|
||||
|
||||
export default DropdownMenuItem;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: string,
|
||||
@@ -14,7 +13,7 @@ const Empty = (props: Props) => {
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
const HelpText = styled.p`
|
||||
margin-top: 0;
|
||||
color: ${color.slateDark};
|
||||
color: ${props => props.theme.slateDark};
|
||||
`;
|
||||
|
||||
export default HelpText;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import replace from 'string-replace-to-array';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
highlight: ?string,
|
||||
@@ -28,7 +27,7 @@ function Highlight({ highlight, caseSensitive, text, ...rest }: Props) {
|
||||
}
|
||||
|
||||
const Mark = styled.mark`
|
||||
background: ${color.yellow};
|
||||
background: ${props => props.theme.yellow};
|
||||
`;
|
||||
|
||||
export default Highlight;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { size, color } from 'shared/styles/constants';
|
||||
|
||||
const RealTextarea = styled.textarea`
|
||||
border: 0;
|
||||
@@ -13,7 +12,7 @@ const RealTextarea = styled.textarea`
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -26,7 +25,7 @@ const RealInput = styled.input`
|
||||
|
||||
&:disabled,
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -37,16 +36,16 @@ const Wrapper = styled.div`
|
||||
export const Outline = styled(Flex)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 0 ${size.large};
|
||||
margin: 0 0 16px;
|
||||
color: inherit;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: ${props => (props.hasError ? 'red' : color.slateLight)};
|
||||
border-color: ${props => (props.hasError ? 'red' : props.theme.slateLight)};
|
||||
border-radius: 4px;
|
||||
font-weight: normal;
|
||||
|
||||
&:focus {
|
||||
border-color: ${color.slate};
|
||||
border-color: ${props => props.theme.slate};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
const Key = styled.kbd`
|
||||
display: inline-block;
|
||||
@@ -8,13 +7,13 @@ const Key = styled.kbd`
|
||||
font: 11px 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
|
||||
monospace;
|
||||
line-height: 10px;
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
vertical-align: middle;
|
||||
background-color: ${color.smokeLight};
|
||||
border: solid 1px ${color.slateLight};
|
||||
border-bottom-color: ${color.slate};
|
||||
background-color: ${props => props.theme.smokeLight};
|
||||
border: solid 1px ${props => props.theme.slateLight};
|
||||
border-bottom-color: ${props => props.theme.slate};
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 ${color.slate};
|
||||
box-shadow: inset 0 -1px 0 ${props => props.theme.slate};
|
||||
`;
|
||||
|
||||
export default Key;
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { size } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
label: React.Node | string,
|
||||
@@ -18,7 +17,7 @@ const Labeled = ({ label, children, ...props }: Props) => (
|
||||
);
|
||||
|
||||
export const Label = styled(Flex)`
|
||||
margin-bottom: ${size.medium};
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { observer, inject } from 'mobx-react';
|
||||
import keydown from 'react-keydown';
|
||||
import Analytics from 'shared/components/Analytics';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { layout } from 'shared/styles/constants';
|
||||
import { documentEditUrl, homeUrl, searchUrl } from 'utils/routeHelpers';
|
||||
|
||||
import { LoadingIndicatorBar } from 'components/LoadingIndicator';
|
||||
@@ -39,7 +38,7 @@ type Props = {
|
||||
class Layout extends React.Component<Props> {
|
||||
scrollable: ?HTMLDivElement;
|
||||
|
||||
@keydown(['/', 't'])
|
||||
@keydown(['/', 't', 'meta+k'])
|
||||
goToSearch(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -114,14 +113,14 @@ const Container = styled(Flex)`
|
||||
|
||||
const Content = styled(Flex)`
|
||||
margin: 0;
|
||||
transition: margin-left 200ms ease-in-out;
|
||||
transition: margin-left 100ms ease-out;
|
||||
|
||||
@media print {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
margin-left: ${props => (props.editMode ? 0 : layout.sidebarWidth)};
|
||||
margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)};
|
||||
`};
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color, fontSize } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
image?: React.Node,
|
||||
@@ -27,7 +26,7 @@ const Wrapper = styled.li`
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid ${color.smokeDark};
|
||||
border-bottom: 1px solid ${props => props.theme.smokeDark};
|
||||
`;
|
||||
|
||||
const Image = styled.div`
|
||||
@@ -36,7 +35,7 @@ const Image = styled.div`
|
||||
`;
|
||||
|
||||
const Heading = styled.h2`
|
||||
font-size: ${fontSize.medium};
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
@@ -46,8 +45,8 @@ const Content = styled.div`
|
||||
|
||||
const Subtitle = styled.p`
|
||||
margin: 0;
|
||||
font-size: ${fontSize.small};
|
||||
color: ${color.slate};
|
||||
font-size: 14px;
|
||||
color: ${props => props.theme.slate};
|
||||
`;
|
||||
|
||||
const Actions = styled.div`
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { pulsate } from 'shared/styles/animations';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { randomInteger } from 'shared/random';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
@@ -26,7 +25,7 @@ const Redacted = styled(Flex)`
|
||||
width: ${props => (props.header ? props.width / 2 : props.width)}%;
|
||||
height: ${props => (props.header ? 28 : 18)}px;
|
||||
margin-bottom: ${props => (props.header ? 18 : 12)}px;
|
||||
background-color: ${color.smokeDark};
|
||||
background-color: ${props => props.theme.smokeDark};
|
||||
animation: ${pulsate} 1.3s infinite;
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -5,7 +5,6 @@ import styled, { injectGlobal } from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import ReactModal from 'react-modal';
|
||||
import { CloseIcon } from 'outline-icons';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
@@ -84,7 +83,7 @@ const Close = styled.a`
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
opacity: 0.5;
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
|
||||
@@ -6,6 +6,7 @@ import UiStore from 'stores/UiStore';
|
||||
import CollectionNew from 'scenes/CollectionNew';
|
||||
import CollectionEdit from 'scenes/CollectionEdit';
|
||||
import CollectionDelete from 'scenes/CollectionDelete';
|
||||
import CollectionExport from 'scenes/CollectionExport';
|
||||
import DocumentDelete from 'scenes/DocumentDelete';
|
||||
import DocumentShare from 'scenes/DocumentShare';
|
||||
import KeyboardShortcuts from 'scenes/KeyboardShortcuts';
|
||||
@@ -45,6 +46,9 @@ class Modals extends React.Component<Props> {
|
||||
<Modal name="collection-delete" title="Delete collection">
|
||||
<CollectionDelete onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="collection-export" title="Export collection">
|
||||
<CollectionExport onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
<Modal name="document-share" title="Share document">
|
||||
<DocumentShare onSubmit={this.handleClose} />
|
||||
</Modal>
|
||||
|
||||
@@ -5,9 +5,16 @@ import { withRouter } from 'react-router-dom';
|
||||
|
||||
class ScrollToTop extends React.Component<*> {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.location !== prevProps.location) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
if (this.props.location.pathname === prevProps.location.pathname) return;
|
||||
|
||||
// exception for when entering or exiting document edit, scroll postion should not reset
|
||||
if (
|
||||
this.props.location.pathname.match(/\/edit\/?$/) ||
|
||||
prevProps.location.pathname.match(/\/edit\/?$/)
|
||||
)
|
||||
return;
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import Scrollable from 'components/Scrollable';
|
||||
import Collections from './components/Collections';
|
||||
import SidebarLink from './components/SidebarLink';
|
||||
import HeaderBlock from './components/HeaderBlock';
|
||||
import Bubble from './components/Bubble';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
@@ -27,6 +28,10 @@ type Props = {
|
||||
|
||||
@observer
|
||||
class MainSidebar extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.documents.fetchDrafts();
|
||||
}
|
||||
|
||||
handleCreateCollection = () => {
|
||||
this.props.ui.setActiveModal('collection-new');
|
||||
};
|
||||
@@ -67,7 +72,7 @@ class MainSidebar extends React.Component<Props> {
|
||||
documents.active ? !documents.active.publishedAt : undefined
|
||||
}
|
||||
>
|
||||
Drafts
|
||||
Drafts <Bubble count={documents.drafts.length} />
|
||||
</SidebarLink>
|
||||
</Section>
|
||||
<Section>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import {
|
||||
DocumentIcon,
|
||||
ProfileIcon,
|
||||
SettingsIcon,
|
||||
CodeIcon,
|
||||
@@ -74,6 +75,11 @@ class SettingsSidebar extends React.Component<Props> {
|
||||
Integrations
|
||||
</SidebarLink>
|
||||
)}
|
||||
{user.isAdmin && (
|
||||
<SidebarLink to="/settings/export" icon={<DocumentIcon />}>
|
||||
Export Data
|
||||
</SidebarLink>
|
||||
)}
|
||||
</Section>
|
||||
</Scrollable>
|
||||
</Flex>
|
||||
|
||||
@@ -7,18 +7,13 @@ import breakpoint from 'styled-components-breakpoint';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { CloseIcon, MenuIcon } from 'outline-icons';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { color, layout } from 'shared/styles/constants';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
history: Object,
|
||||
location: Location,
|
||||
auth: AuthStore,
|
||||
documents: DocumentsStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@@ -59,20 +54,36 @@ const Container = styled(Flex)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: ${props => (props.editMode ? `-${layout.sidebarWidth}` : 0)};
|
||||
left: ${props => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
|
||||
width: 100%;
|
||||
background: ${color.smoke};
|
||||
transition: left 200ms ease-in-out;
|
||||
background: ${props => props.theme.smoke};
|
||||
transition: left 100ms ease-out;
|
||||
margin-left: ${props => (props.mobileSidebarVisible ? 0 : '-100%')};
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
background: ${props => props.theme.smoke};
|
||||
position: absolute;
|
||||
top: -50vh;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
&:after {
|
||||
top: auto;
|
||||
bottom: -50vh;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
width: ${layout.sidebarWidth};
|
||||
width: ${props => props.theme.sidebarWidth};
|
||||
margin: 0;
|
||||
`};
|
||||
`;
|
||||
@@ -90,7 +101,7 @@ const Toggle = styled.a`
|
||||
left: ${props => (props.mobileSidebarVisible ? 'auto' : 0)};
|
||||
right: ${props => (props.mobileSidebarVisible ? 0 : 'auto')};
|
||||
z-index: 1;
|
||||
margin: 16px;
|
||||
margin: 12px;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
display: none;
|
||||
|
||||
31
app/components/Sidebar/components/Bubble.js
Normal file
31
app/components/Sidebar/components/Bubble.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
|
||||
type Props = {
|
||||
count: number,
|
||||
};
|
||||
|
||||
const Bubble = ({ count }: Props) => {
|
||||
return !!count && <Wrapper>{count}</Wrapper>;
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
|
||||
border-radius: 100%;
|
||||
color: ${props => props.theme.white};
|
||||
background: ${props => props.theme.slateDark};
|
||||
display: inline-block;
|
||||
min-width: 15px;
|
||||
padding: 0 5px;
|
||||
font-size: 10px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default Bubble;
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import Flex from 'shared/components/Flex';
|
||||
import styled from 'styled-components';
|
||||
import { color, fontWeight } from 'shared/styles/constants';
|
||||
|
||||
const Header = styled(Flex)`
|
||||
font-size: 11px;
|
||||
font-weight: ${fontWeight.semiBold};
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: ${color.slateDark};
|
||||
color: ${props => props.theme.slateDark};
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { ExpandedIcon } from 'outline-icons';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import TeamLogo from './TeamLogo';
|
||||
@@ -11,6 +10,7 @@ type Props = {
|
||||
subheading: string,
|
||||
showDisclosure?: boolean,
|
||||
logoUrl: string,
|
||||
theme: Object,
|
||||
};
|
||||
|
||||
function HeaderBlock({
|
||||
@@ -18,15 +18,16 @@ function HeaderBlock({
|
||||
teamName,
|
||||
subheading,
|
||||
logoUrl,
|
||||
theme,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<Header justify="flex-start" align="center" {...rest}>
|
||||
<TeamLogo src={logoUrl} />
|
||||
<TeamLogo alt={`${teamName} logo`} src={logoUrl} />
|
||||
<Flex align="flex-start" column>
|
||||
<TeamName showDisclosure>
|
||||
{teamName}{' '}
|
||||
{showDisclosure && <StyledExpandedIcon color={color.text} />}
|
||||
{showDisclosure && <StyledExpandedIcon color={theme.text} />}
|
||||
</TeamName>
|
||||
<Subheading>{subheading}</Subheading>
|
||||
</Flex>
|
||||
@@ -45,7 +46,7 @@ const Subheading = styled.div`
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
color: ${color.slateDark};
|
||||
color: ${props => props.theme.slateDark};
|
||||
`;
|
||||
|
||||
const TeamName = styled.div`
|
||||
@@ -53,7 +54,7 @@ const TeamName = styled.div`
|
||||
padding-left: 10px;
|
||||
padding-right: 24px;
|
||||
font-weight: 600;
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
`;
|
||||
@@ -72,4 +73,4 @@ const Header = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default HeaderBlock;
|
||||
export default withTheme(HeaderBlock);
|
||||
|
||||
@@ -4,15 +4,9 @@ import { observable, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { withRouter, NavLink } from 'react-router-dom';
|
||||
import { CollapsedIcon } from 'outline-icons';
|
||||
import { color, fontWeight } from 'shared/styles/constants';
|
||||
import styled from 'styled-components';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
const activeStyle = {
|
||||
color: color.black,
|
||||
fontWeight: fontWeight.medium,
|
||||
};
|
||||
|
||||
const StyledGoTo = styled(CollapsedIcon)`
|
||||
margin-bottom: -4px;
|
||||
margin-left: 1px;
|
||||
@@ -34,12 +28,12 @@ const StyledNavLink = styled(NavLink)`
|
||||
text-overflow: ellipsis;
|
||||
padding: 4px 0;
|
||||
margin-left: ${({ icon }) => (icon ? '-20px;' : '0')};
|
||||
color: ${color.slateDark};
|
||||
color: ${props => props.theme.slateDark};
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -57,11 +51,22 @@ type Props = {
|
||||
hideExpandToggle?: boolean,
|
||||
iconColor?: string,
|
||||
active?: boolean,
|
||||
theme: Object,
|
||||
};
|
||||
|
||||
@observer
|
||||
class SidebarLink extends React.Component<Props> {
|
||||
@observable expanded: boolean = false;
|
||||
activeStyle: Object;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.activeStyle = {
|
||||
color: props.theme.black,
|
||||
fontWeight: 500,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.expand) this.handleExpand();
|
||||
@@ -104,8 +109,8 @@ class SidebarLink extends React.Component<Props> {
|
||||
<Wrapper menuOpen={menuOpen} column>
|
||||
<Component
|
||||
icon={showExpandIcon}
|
||||
activeStyle={activeStyle}
|
||||
style={active ? activeStyle : undefined}
|
||||
activeStyle={this.activeStyle}
|
||||
style={active ? this.activeStyle : undefined}
|
||||
onClick={onClick}
|
||||
to={to}
|
||||
exact
|
||||
@@ -128,7 +133,7 @@ const Action = styled.span`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
svg {
|
||||
opacity: 0.75;
|
||||
}
|
||||
@@ -158,4 +163,4 @@ const Content = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default withRouter(SidebarLink);
|
||||
export default withRouter(withTheme(SidebarLink));
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
const TeamLogo = styled.img`
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
background: ${color.white};
|
||||
border: 1px solid ${color.slateLight};
|
||||
background: ${props => props.theme.white};
|
||||
border: 1px solid ${props => props.theme.slateLight};
|
||||
`;
|
||||
|
||||
export default TeamLogo;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
const Subheading = styled.h3`
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: ${color.slate};
|
||||
color: ${props => props.theme.slate};
|
||||
letter-spacing: 0.04em;
|
||||
border-bottom: 1px solid ${color.slateLight};
|
||||
border-bottom: 1px solid ${props => props.theme.slateLight};
|
||||
padding-bottom: 8px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { layout } from 'shared/styles/constants';
|
||||
import Toast from './components/Toast';
|
||||
import UiStore from '../../stores/UiStore';
|
||||
|
||||
@@ -34,8 +33,8 @@ class Toasts extends React.Component<Props> {
|
||||
|
||||
const List = styled.ol`
|
||||
position: fixed;
|
||||
left: ${layout.hpadding};
|
||||
bottom: ${layout.vpadding};
|
||||
left: ${props => props.theme.hpadding};
|
||||
bottom: ${props => props.theme.vpadding};
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { darken } from 'polished';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import { fadeAndScaleIn } from 'shared/styles/animations';
|
||||
import type { Toast as TToast } from '../../../types';
|
||||
|
||||
@@ -51,14 +50,14 @@ const Container = styled.li`
|
||||
animation: ${fadeAndScaleIn} 100ms ease;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
color: ${color.white};
|
||||
background: ${props => color[props.type]};
|
||||
color: ${props => props.theme.white};
|
||||
background: ${props => props.theme[props.type]};
|
||||
font-size: 15px;
|
||||
border-radius: 5px;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: ${props => darken(0.05, color[props.type])};
|
||||
background: ${props => darken(0.05, props.theme[props.type])};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
156
app/index.js
156
app/index.js
@@ -2,6 +2,7 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import stores from 'stores';
|
||||
import theme from 'shared/styles/theme';
|
||||
import globalStyles from 'shared/styles/globals';
|
||||
import 'shared/styles/prism.css';
|
||||
|
||||
@@ -26,6 +28,7 @@ import People from 'scenes/Settings/People';
|
||||
import Slack from 'scenes/Settings/Slack';
|
||||
import Shares from 'scenes/Settings/Shares';
|
||||
import Tokens from 'scenes/Settings/Tokens';
|
||||
import Export from 'scenes/Settings/Export';
|
||||
import Error404 from 'scenes/Error404';
|
||||
|
||||
import ErrorBoundary from 'components/ErrorBoundary';
|
||||
@@ -55,73 +58,92 @@ if (element) {
|
||||
render(
|
||||
<React.Fragment>
|
||||
<ErrorBoundary>
|
||||
<Provider {...stores}>
|
||||
<Router>
|
||||
<ScrollToTop>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/share/:shareId" component={Document} />
|
||||
<Auth>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/details"
|
||||
component={Details}
|
||||
/>
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/shares" component={Shares} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id"
|
||||
component={Collection}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/d/${matchDocumentSlug}`}
|
||||
component={RedirectDocument}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/move`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:query" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/edit`}
|
||||
component={Document}
|
||||
/>
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path="/collections/:id/new"
|
||||
component={DocumentNew}
|
||||
/>
|
||||
<Route component={notFoundSearch} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</Auth>
|
||||
</Switch>
|
||||
</ScrollToTop>
|
||||
</Router>
|
||||
</Provider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider {...stores}>
|
||||
<Router>
|
||||
<ScrollToTop>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/share/:shareId" component={Document} />
|
||||
<Auth>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route exact path="/dashboard" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/details"
|
||||
component={Details}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/people"
|
||||
component={People}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/shares"
|
||||
component={Shares}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/tokens"
|
||||
component={Tokens}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/export"
|
||||
component={Export}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id"
|
||||
component={Collection}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/d/${matchDocumentSlug}`}
|
||||
component={RedirectDocument}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/move`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:query" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/edit`}
|
||||
component={Document}
|
||||
/>
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path="/collections/:id/new"
|
||||
component={DocumentNew}
|
||||
/>
|
||||
<Route component={notFoundSearch} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</Auth>
|
||||
</Switch>
|
||||
</ScrollToTop>
|
||||
</Router>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
|
||||
</React.Fragment>,
|
||||
|
||||
@@ -61,6 +61,12 @@ class CollectionMenu extends React.Component<Props> {
|
||||
this.props.ui.setActiveModal('collection-delete', { collection });
|
||||
};
|
||||
|
||||
onExport = (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
const { collection } = this.props;
|
||||
this.props.ui.setActiveModal('collection-export', { collection });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collection, label, onOpen, onClose } = this.props;
|
||||
|
||||
@@ -87,6 +93,9 @@ class CollectionMenu extends React.Component<Props> {
|
||||
</DropdownMenuItem>
|
||||
<hr />
|
||||
<DropdownMenuItem onClick={this.onEdit}>Edit…</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={this.onExport}>
|
||||
Export…
|
||||
</DropdownMenuItem>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<DropdownMenuItem onClick={this.onDelete}>Delete…</DropdownMenuItem>
|
||||
|
||||
53
app/menus/NewDocumentMenu.js
Normal file
53
app/menus/NewDocumentMenu.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { inject } from 'mobx-react';
|
||||
import { MoreIcon, CollectionIcon } from 'outline-icons';
|
||||
|
||||
import { newDocumentUrl } from 'utils/routeHelpers';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
|
||||
type Props = {
|
||||
label?: React.Node,
|
||||
history: Object,
|
||||
collections: CollectionsStore,
|
||||
};
|
||||
|
||||
class NewDocumentMenu extends React.Component<Props> {
|
||||
handleNewDocument = collection => {
|
||||
this.props.history.push(newDocumentUrl(collection));
|
||||
};
|
||||
|
||||
onOpen = () => {
|
||||
const { collections } = this.props;
|
||||
|
||||
if (collections.orderedData.length === 1) {
|
||||
this.handleNewDocument(collections.orderedData[0]);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collections, label, history, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
label={label || <MoreIcon />}
|
||||
onOpen={this.onOpen}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuItem disabled>Choose a collection…</DropdownMenuItem>
|
||||
{collections.orderedData.map(collection => (
|
||||
<DropdownMenuItem
|
||||
key={collection.id}
|
||||
onClick={() => this.handleNewDocument(collection)}
|
||||
>
|
||||
<CollectionIcon color={collection.color} /> {collection.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(inject('collections')(NewDocumentMenu));
|
||||
@@ -133,6 +133,11 @@ class Collection extends BaseModel {
|
||||
return false;
|
||||
};
|
||||
|
||||
@action
|
||||
export = async () => {
|
||||
await client.post('/collections.export', { id: this.id });
|
||||
};
|
||||
|
||||
@action
|
||||
updateData(data: Object = {}) {
|
||||
this.data = data;
|
||||
|
||||
@@ -6,8 +6,9 @@ import { client } from 'utils/ApiClient';
|
||||
import stores from 'stores';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import parseTitle from '../../shared/utils/parseTitle';
|
||||
import unescape from '../../shared/utils/unescape';
|
||||
|
||||
import type { User } from 'types';
|
||||
import type { NavigationNode, User } from 'types';
|
||||
import BaseModel from './BaseModel';
|
||||
import Collection from './Collection';
|
||||
|
||||
@@ -51,17 +52,11 @@ class Document extends BaseModel {
|
||||
}
|
||||
|
||||
@computed
|
||||
get pathToDocument(): Array<{ id: string, title: string }> {
|
||||
get pathToDocument(): NavigationNode[] {
|
||||
let path;
|
||||
const traveler = (nodes, previousPath) => {
|
||||
nodes.forEach(childNode => {
|
||||
const newPath = [
|
||||
...previousPath,
|
||||
{
|
||||
id: childNode.id,
|
||||
title: childNode.title,
|
||||
},
|
||||
];
|
||||
const newPath = [...previousPath, childNode];
|
||||
if (childNode.id === this.id) {
|
||||
path = newPath;
|
||||
return;
|
||||
@@ -180,19 +175,12 @@ class Document extends BaseModel {
|
||||
if (this.isSaving) return this;
|
||||
|
||||
const wasDraft = !this.publishedAt;
|
||||
const isCreating = !this.id;
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (this.id) {
|
||||
res = await client.post('/documents.update', {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
text: this.text,
|
||||
lastRevision: this.revision,
|
||||
...options,
|
||||
});
|
||||
} else {
|
||||
if (isCreating) {
|
||||
const data = {
|
||||
parentDocument: undefined,
|
||||
collection: this.collection.id,
|
||||
@@ -204,25 +192,36 @@ class Document extends BaseModel {
|
||||
data.parentDocument = this.parentDocument;
|
||||
}
|
||||
res = await client.post('/documents.create', data);
|
||||
if (res && res.data) this.emit('documents.create', res.data);
|
||||
} else {
|
||||
res = await client.post('/documents.update', {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
text: this.text,
|
||||
lastRevision: this.revision,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
runInAction('Document#save', () => {
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
this.updateData(res.data);
|
||||
this.hasPendingChanges = false;
|
||||
});
|
||||
|
||||
this.emit('documents.update', {
|
||||
document: this,
|
||||
collectionId: this.collection.id,
|
||||
});
|
||||
if (isCreating) {
|
||||
this.emit('documents.create', this);
|
||||
}
|
||||
|
||||
if (wasDraft && this.publishedAt) {
|
||||
this.emit('documents.publish', {
|
||||
id: this.id,
|
||||
this.emit('documents.update', {
|
||||
document: this,
|
||||
collectionId: this.collection.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (wasDraft && this.publishedAt) {
|
||||
this.emit('documents.publish', {
|
||||
id: this.id,
|
||||
collectionId: this.collection.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.ui.showToast('Document failed to save');
|
||||
} finally {
|
||||
@@ -270,15 +269,16 @@ class Document extends BaseModel {
|
||||
this.emit('documents.duplicate', this);
|
||||
};
|
||||
|
||||
download() {
|
||||
const a = window.document.createElement('a');
|
||||
a.textContent = 'download';
|
||||
download = async () => {
|
||||
await this.fetch();
|
||||
|
||||
const blob = new Blob([unescape(this.text)], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${this.title}.md`;
|
||||
a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(
|
||||
this.text
|
||||
)}`;
|
||||
a.click();
|
||||
}
|
||||
};
|
||||
|
||||
updateData(data: Object = {}, dirty: boolean = false) {
|
||||
if (data.text) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Collection from './Collection';
|
||||
export default Collection;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import CollectionDelete from './CollectionDelete';
|
||||
export default CollectionDelete;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import CollectionEdit from './CollectionEdit';
|
||||
export default CollectionEdit;
|
||||
55
app/scenes/CollectionExport.js
Normal file
55
app/scenes/CollectionExport.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import Button from 'components/Button';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Collection from 'models/Collection';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
onSubmit: () => void,
|
||||
};
|
||||
|
||||
@observer
|
||||
class CollectionExport extends React.Component<Props> {
|
||||
@observable isLoading: boolean = false;
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
this.isLoading = true;
|
||||
await this.props.collection.export();
|
||||
this.isLoading = false;
|
||||
|
||||
this.props.ui.showToast('Export in progress…', 'success');
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collection, auth } = this.props;
|
||||
if (!auth.user) return;
|
||||
|
||||
return (
|
||||
<Flex column>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<HelpText>
|
||||
Exporting the collection <strong>{collection.name}</strong> may take
|
||||
a few minutes. We’ll put together a zip file of your documents in
|
||||
Markdown format and email it to <strong>{auth.user.email}</strong>.
|
||||
</HelpText>
|
||||
<Button type="submit" disabled={this.isLoading} primary>
|
||||
{this.isLoading ? 'Requesting Export…' : 'Export Collection'}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('ui', 'auth')(CollectionExport);
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import CollectionNew from './CollectionNew';
|
||||
export default CollectionNew;
|
||||
@@ -2,8 +2,11 @@
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
|
||||
import NewDocumentMenu from 'menus/NewDocumentMenu';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import Actions, { Action } from 'components/Actions';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import DocumentList from 'components/DocumentList';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
@@ -42,22 +45,33 @@ class Dashboard extends React.Component<Props> {
|
||||
<PageTitle title="Home" />
|
||||
<h1>Home</h1>
|
||||
{showContent ? (
|
||||
<span>
|
||||
{hasRecentlyViewed && [
|
||||
<Subheading key="viewed">Recently viewed</Subheading>,
|
||||
<DocumentList
|
||||
key="viewedDocuments"
|
||||
documents={documents.recentlyViewed}
|
||||
/>,
|
||||
]}
|
||||
{hasRecentlyEdited && [
|
||||
<Subheading key="edited">Recently edited</Subheading>,
|
||||
<DocumentList
|
||||
key="editedDocuments"
|
||||
documents={documents.recentlyEdited}
|
||||
/>,
|
||||
]}
|
||||
</span>
|
||||
<React.Fragment>
|
||||
{hasRecentlyViewed && (
|
||||
<React.Fragment>
|
||||
<Subheading key="viewed">Recently viewed</Subheading>
|
||||
<DocumentList
|
||||
key="viewedDocuments"
|
||||
documents={documents.recentlyViewed}
|
||||
showCollection
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{hasRecentlyEdited && (
|
||||
<React.Fragment>
|
||||
<Subheading key="edited">Recently edited</Subheading>
|
||||
<DocumentList
|
||||
key="editedDocuments"
|
||||
documents={documents.recentlyEdited}
|
||||
showCollection
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<NewDocumentMenu label={<NewDocumentIcon />} />
|
||||
</Action>
|
||||
</Actions>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<ListPlaceholder count={5} />
|
||||
)}
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
matchDocumentMove,
|
||||
} from 'utils/routeHelpers';
|
||||
import { uploadFile } from 'utils/uploadFile';
|
||||
import { emojiToUrl } from 'utils/emoji';
|
||||
import isInternalUrl from 'utils/isInternalUrl';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import Actions from './components/Actions';
|
||||
import Header from './components/Header';
|
||||
import DocumentMove from './components/DocumentMove';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
@@ -40,6 +41,10 @@ const DISCARD_CHANGES = `
|
||||
You have unsaved changes.
|
||||
Are you sure you want to discard them?
|
||||
`;
|
||||
const UPLOADING_WARNING = `
|
||||
Image are still uploading.
|
||||
Are you sure you want to discard them?
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
match: Object,
|
||||
@@ -51,25 +56,6 @@ type Props = {
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
function toCodePoint(unicodeSurrogates, sep) {
|
||||
var r = [],
|
||||
c = 0,
|
||||
p = 0,
|
||||
i = 0;
|
||||
while (i < unicodeSurrogates.length) {
|
||||
c = unicodeSurrogates.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xd800) << 10) + (c - 0xdc00)).toString(16));
|
||||
p = 0;
|
||||
} else if (0xd800 <= c && c <= 0xdbff) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
return r.join(sep || '-');
|
||||
}
|
||||
|
||||
@observer
|
||||
class DocumentScene extends React.Component<Props> {
|
||||
savedTimeout: TimeoutID;
|
||||
@@ -79,7 +65,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
@observable editCache: ?string;
|
||||
@observable document: ?Document;
|
||||
@observable newDocument: ?Document;
|
||||
@observable isLoading = false;
|
||||
@observable isUploading = false;
|
||||
@observable isSaving = false;
|
||||
@observable isPublishing = false;
|
||||
@observable notFound = false;
|
||||
@@ -155,7 +141,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
};
|
||||
|
||||
loadEditor = async () => {
|
||||
const EditorImport = await import('rich-markdown-editor');
|
||||
const EditorImport = await import('./components/Editor');
|
||||
this.editorComponent = EditorImport.default;
|
||||
};
|
||||
|
||||
@@ -199,11 +185,11 @@ class DocumentScene extends React.Component<Props> {
|
||||
}, AUTOSAVE_INTERVAL);
|
||||
|
||||
onImageUploadStart = () => {
|
||||
this.isLoading = true;
|
||||
this.isUploading = true;
|
||||
};
|
||||
|
||||
onImageUploadStop = () => {
|
||||
this.isLoading = false;
|
||||
this.isUploading = false;
|
||||
};
|
||||
|
||||
onChange = text => {
|
||||
@@ -249,7 +235,14 @@ class DocumentScene extends React.Component<Props> {
|
||||
|
||||
onClickLink = (href: string) => {
|
||||
if (isInternalUrl(href)) {
|
||||
this.props.history.push(href);
|
||||
// relative
|
||||
if (href[0] === '/') {
|
||||
this.props.history.push(href);
|
||||
}
|
||||
|
||||
// absolute
|
||||
const url = new URL(href);
|
||||
this.props.history.push(url.pathname);
|
||||
} else {
|
||||
window.open(href, '_blank');
|
||||
}
|
||||
@@ -260,8 +253,6 @@ class DocumentScene extends React.Component<Props> {
|
||||
const Editor = this.editorComponent;
|
||||
const isMoving = match.path === matchDocumentMove;
|
||||
const document = this.document;
|
||||
const titleFromState = location.state ? location.state.title : '';
|
||||
const titleText = document ? document.title : titleFromState;
|
||||
const isShare = match.params.shareId;
|
||||
|
||||
if (this.notFound) {
|
||||
@@ -276,82 +267,82 @@ class DocumentScene extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
let favicon;
|
||||
if (document && document.emoji) {
|
||||
favicon = `https://twemoji.maxcdn.com/2/72x72/${toCodePoint(
|
||||
document.emoji
|
||||
)}.png`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container key={document ? document.id : undefined} column auto>
|
||||
{isMoving && document && <DocumentMove document={document} />}
|
||||
{titleText && (
|
||||
<PageTitle
|
||||
title={titleText.replace(document.emoji, '')}
|
||||
favicon={favicon}
|
||||
/>
|
||||
)}
|
||||
{(this.isLoading || this.isSaving) && <LoadingIndicator />}
|
||||
{!document || !Editor ? (
|
||||
if (!document || !Editor) {
|
||||
return (
|
||||
<Container column auto>
|
||||
<PageTitle title={location.state ? location.state.title : ''} />
|
||||
<CenteredContent>
|
||||
<LoadingState />
|
||||
</CenteredContent>
|
||||
) : (
|
||||
<Flex justify="center" auto>
|
||||
{this.isEditing && (
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container key={document.id} column auto>
|
||||
{isMoving && <DocumentMove document={document} />}
|
||||
<PageTitle
|
||||
title={document.title.replace(document.emoji, '')}
|
||||
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
|
||||
/>
|
||||
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
|
||||
|
||||
<Container justify="center" column auto>
|
||||
{this.isEditing && (
|
||||
<React.Fragment>
|
||||
<Prompt
|
||||
when={document.hasPendingChanges}
|
||||
message={DISCARD_CHANGES}
|
||||
/>
|
||||
)}
|
||||
<MaxWidth column auto>
|
||||
<Editor
|
||||
titlePlaceholder="Start with a title…"
|
||||
bodyPlaceholder="…the rest is your canvas"
|
||||
defaultValue={document.text}
|
||||
pretitle={document.emoji}
|
||||
uploadImage={this.onUploadImage}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onSearchLink={this.onSearchLink}
|
||||
onClickLink={this.onClickLink}
|
||||
onChange={this.onChange}
|
||||
onSave={this.onSave}
|
||||
onCancel={this.onDiscard}
|
||||
readOnly={!this.isEditing}
|
||||
toc
|
||||
/>
|
||||
</MaxWidth>
|
||||
{document &&
|
||||
!isShare && (
|
||||
<Actions
|
||||
document={document}
|
||||
isDraft={!document.publishedAt}
|
||||
isEditing={this.isEditing}
|
||||
isSaving={this.isSaving}
|
||||
isPublishing={this.isPublishing}
|
||||
savingIsDisabled={!document.allowSave}
|
||||
history={this.props.history}
|
||||
onDiscard={this.onDiscard}
|
||||
onSave={this.onSave}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Prompt when={this.isUploading} message={UPLOADING_WARNING} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!isShare && (
|
||||
<Header
|
||||
document={document}
|
||||
isDraft={!document.publishedAt}
|
||||
isEditing={this.isEditing}
|
||||
isSaving={this.isSaving}
|
||||
isPublishing={this.isPublishing}
|
||||
savingIsDisabled={!document.allowSave}
|
||||
history={this.props.history}
|
||||
onDiscard={this.onDiscard}
|
||||
onSave={this.onSave}
|
||||
/>
|
||||
)}
|
||||
<MaxWidth column auto>
|
||||
<Editor
|
||||
titlePlaceholder="Start with a title…"
|
||||
bodyPlaceholder="…the rest is your canvas"
|
||||
defaultValue={document.text}
|
||||
pretitle={document.emoji}
|
||||
uploadImage={this.onUploadImage}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onSearchLink={this.onSearchLink}
|
||||
onClickLink={this.onClickLink}
|
||||
onChange={this.onChange}
|
||||
onSave={this.onSave}
|
||||
onCancel={this.onDiscard}
|
||||
readOnly={!this.isEditing}
|
||||
toc
|
||||
/>
|
||||
</MaxWidth>
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MaxWidth = styled(Flex)`
|
||||
padding: 0 20px;
|
||||
padding: 0 16px;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
padding: 0;
|
||||
margin: 60px;
|
||||
margin: 12px auto;
|
||||
max-width: 46em;
|
||||
`};
|
||||
`;
|
||||
@@ -361,7 +352,7 @@ const Container = styled(Flex)`
|
||||
`;
|
||||
|
||||
const LoadingState = styled(LoadingPlaceholder)`
|
||||
margin: 90px 0;
|
||||
margin: 40px 0;
|
||||
`;
|
||||
|
||||
export default withRouter(inject('ui', 'auth', 'documents')(DocumentScene));
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Document from 'models/Document';
|
||||
import { documentEditUrl, documentNewUrl } from 'utils/routeHelpers';
|
||||
|
||||
import DocumentMenu from 'menus/DocumentMenu';
|
||||
import Collaborators from 'components/Collaborators';
|
||||
import Actions, { Action, Separator } from 'components/Actions';
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
isDraft: boolean,
|
||||
isEditing: boolean,
|
||||
isSaving: boolean,
|
||||
isPublishing: boolean,
|
||||
savingIsDisabled: boolean,
|
||||
onDiscard: () => *,
|
||||
onSave: ({
|
||||
done?: boolean,
|
||||
publish?: boolean,
|
||||
autosave?: boolean,
|
||||
}) => *,
|
||||
history: Object,
|
||||
};
|
||||
|
||||
class DocumentActions extends React.Component<Props> {
|
||||
handleNewDocument = () => {
|
||||
this.props.history.push(documentNewUrl(this.props.document));
|
||||
};
|
||||
|
||||
handleEdit = () => {
|
||||
this.props.history.push(documentEditUrl(this.props.document));
|
||||
};
|
||||
|
||||
handleSave = () => {
|
||||
this.props.onSave({ done: true });
|
||||
};
|
||||
|
||||
handlePublish = () => {
|
||||
this.props.onSave({ done: true, publish: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
document,
|
||||
isEditing,
|
||||
isDraft,
|
||||
isPublishing,
|
||||
isSaving,
|
||||
savingIsDisabled,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Actions align="center" justify="flex-end" readOnly={!isEditing}>
|
||||
{!isDraft && !isEditing && <Collaborators document={document} />}
|
||||
{isDraft && (
|
||||
<Action>
|
||||
<Link
|
||||
onClick={this.handlePublish}
|
||||
title="Publish document (Cmd+Enter)"
|
||||
disabled={savingIsDisabled}
|
||||
highlight
|
||||
>
|
||||
{isPublishing ? 'Publishing…' : 'Publish'}
|
||||
</Link>
|
||||
</Action>
|
||||
)}
|
||||
{isEditing && (
|
||||
<React.Fragment>
|
||||
<Action>
|
||||
<Link
|
||||
onClick={this.handleSave}
|
||||
title="Save changes (Cmd+Enter)"
|
||||
disabled={savingIsDisabled}
|
||||
isSaving={isSaving}
|
||||
highlight={!isDraft}
|
||||
>
|
||||
{isSaving && !isPublishing ? 'Saving…' : 'Save'}
|
||||
</Link>
|
||||
</Action>
|
||||
{isDraft && <Separator />}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Action>
|
||||
<a onClick={this.handleEdit}>Edit</a>
|
||||
</Action>
|
||||
)}
|
||||
{isEditing && (
|
||||
<Action>
|
||||
<a onClick={this.props.onDiscard}>
|
||||
{document.hasPendingChanges ? 'Discard' : 'Done'}
|
||||
</a>
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Action>
|
||||
<DocumentMenu document={document} showPrint />
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing &&
|
||||
!isDraft && (
|
||||
<React.Fragment>
|
||||
<Separator />
|
||||
<Action>
|
||||
<a onClick={this.handleNewDocument}>
|
||||
<NewDocumentIcon />
|
||||
</a>
|
||||
</Action>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Link = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: ${props => (props.highlight ? 500 : 'inherit')};
|
||||
color: ${props =>
|
||||
props.highlight ? `${color.primary} !important` : 'inherit'};
|
||||
opacity: ${props => (props.disabled ? 0.5 : 1)};
|
||||
pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
|
||||
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
|
||||
`;
|
||||
|
||||
export default DocumentActions;
|
||||
76
app/scenes/Document/components/Breadcrumb.js
Normal file
76
app/scenes/Document/components/Breadcrumb.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CollectionIcon, GoToIcon } from 'outline-icons';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import { collectionUrl } from 'utils/routeHelpers';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
collections: CollectionsStore,
|
||||
};
|
||||
|
||||
const Breadcrumb = observer(({ document, collections }: Props) => {
|
||||
const path = document.pathToDocument.slice(0, -1);
|
||||
const collection =
|
||||
collections.getById(document.collection.id) || document.collection;
|
||||
|
||||
return (
|
||||
<Wrapper justify="flex-start" align="center">
|
||||
<CollectionName to={collectionUrl(collection.id)}>
|
||||
<CollectionIcon color={collection.color} expanded />{' '}
|
||||
<span>{collection.name}</span>
|
||||
</CollectionName>
|
||||
{path.map(n => (
|
||||
<React.Fragment key={n.id}>
|
||||
<Slash /> <Crumb to={n.url}>{n.title}</Crumb>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
});
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
width: 33.3%;
|
||||
display: none;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
display: flex;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Slash = styled(GoToIcon)`
|
||||
flex-shrink: 0;
|
||||
opacity: 0.25;
|
||||
`;
|
||||
|
||||
const Crumb = styled(Link)`
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 15px;
|
||||
height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
const CollectionName = styled(Link)`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
color: ${props => props.theme.text};
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export default inject('collections')(Breadcrumb);
|
||||
@@ -8,7 +8,6 @@ import { Search } from 'js-search';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { size } from 'shared/styles/constants';
|
||||
|
||||
import Modal from 'components/Modal';
|
||||
import Input from 'components/Input';
|
||||
@@ -181,7 +180,7 @@ class DocumentMove extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const Section = styled(Flex)`
|
||||
margin-bottom: ${size.huge};
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
|
||||
@@ -4,8 +4,6 @@ import { observer } from 'mobx-react';
|
||||
import invariant from 'invariant';
|
||||
import styled from 'styled-components';
|
||||
import { GoToIcon } from 'outline-icons';
|
||||
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
import Document from 'models/Document';
|
||||
@@ -14,7 +12,7 @@ const ResultWrapper = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
color: ${color.text};
|
||||
color: ${props => props.theme.text};
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
@@ -30,13 +28,13 @@ const ResultWrapperLink = ResultWrapper.withComponent('a').extend`
|
||||
&:focus {
|
||||
margin-left: 0px;
|
||||
border-radius: 2px;
|
||||
background: ${color.black};
|
||||
color: ${color.smokeLight};
|
||||
background: ${props => props.theme.black};
|
||||
color: ${props => props.theme.smokeLight};
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
${StyledGoToIcon} {
|
||||
fill: ${color.white};
|
||||
fill: ${props => props.theme.white};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
111
app/scenes/Document/components/Editor.js
Normal file
111
app/scenes/Document/components/Editor.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Block, Change, Node, Mark, Text } from 'slate';
|
||||
import RichMarkdownEditor, { Placeholder, schema } from 'rich-markdown-editor';
|
||||
import ClickablePadding from 'components/ClickablePadding';
|
||||
|
||||
type Props = {
|
||||
titlePlaceholder: string,
|
||||
bodyPlaceholder: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
// add rules to the schema to ensure the first node is a heading
|
||||
schema.document.nodes.unshift({ types: ['heading1'], min: 1, max: 1 });
|
||||
schema.document.normalize = (
|
||||
change: Change,
|
||||
reason: string,
|
||||
{
|
||||
node,
|
||||
child,
|
||||
mark,
|
||||
index,
|
||||
}: { node: Node, mark?: Mark, child: Node, index: number }
|
||||
) => {
|
||||
switch (reason) {
|
||||
case 'child_type_invalid': {
|
||||
return change.setNodeByKey(
|
||||
child.key,
|
||||
index === 0 ? 'heading1' : 'paragraph'
|
||||
);
|
||||
}
|
||||
case 'child_required': {
|
||||
const block = Block.create(index === 0 ? 'heading1' : 'paragraph');
|
||||
return change.insertNodeByKey(node.key, index, block);
|
||||
}
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
class Editor extends React.Component<Props> {
|
||||
editor: *;
|
||||
|
||||
setEditorRef = (ref: RichMarkdownEditor) => {
|
||||
this.editor = ref;
|
||||
};
|
||||
|
||||
focusAtEnd = () => {
|
||||
if (this.editor) this.editor.focusAtEnd();
|
||||
};
|
||||
|
||||
renderPlaceholder = (props: *) => {
|
||||
const { editor, node } = props;
|
||||
|
||||
if (editor.state.isComposing) return;
|
||||
if (node.object !== 'block') return;
|
||||
if (!Text.isTextList(node.nodes)) return;
|
||||
if (node.text !== '') return;
|
||||
|
||||
const index = editor.value.document.getBlocks().indexOf(node);
|
||||
if (index > 1) return;
|
||||
|
||||
const text =
|
||||
index === 0 ? this.props.titlePlaceholder : this.props.bodyPlaceholder;
|
||||
|
||||
return <Placeholder>{editor.props.readOnly ? '' : text}</Placeholder>;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { readOnly } = this.props;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<StyledEditor
|
||||
innerRef={this.setEditorRef}
|
||||
renderPlaceholder={this.renderPlaceholder}
|
||||
schema={schema}
|
||||
{...this.props}
|
||||
/>
|
||||
<ClickablePadding
|
||||
onClick={!readOnly ? this.focusAtEnd : undefined}
|
||||
grow
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// additional styles account for placeholder nodes not always re-rendering
|
||||
const StyledEditor = styled(RichMarkdownEditor)`
|
||||
display: flex;
|
||||
flex: 0;
|
||||
|
||||
${Placeholder} {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:nth-child(2):last-child {
|
||||
${Placeholder} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Editor;
|
||||
232
app/scenes/Document/components/Header.js
Normal file
232
app/scenes/Document/components/Header.js
Normal file
@@ -0,0 +1,232 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import breakpoint from 'styled-components-breakpoint';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
import Document from 'models/Document';
|
||||
import { documentEditUrl, documentNewUrl } from 'utils/routeHelpers';
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Breadcrumb from './Breadcrumb';
|
||||
import DocumentMenu from 'menus/DocumentMenu';
|
||||
import Collaborators from 'components/Collaborators';
|
||||
import { Action, Separator } from 'components/Actions';
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
isDraft: boolean,
|
||||
isEditing: boolean,
|
||||
isSaving: boolean,
|
||||
isPublishing: boolean,
|
||||
savingIsDisabled: boolean,
|
||||
onDiscard: () => *,
|
||||
onSave: ({
|
||||
done?: boolean,
|
||||
publish?: boolean,
|
||||
autosave?: boolean,
|
||||
}) => *,
|
||||
history: Object,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Header extends React.Component<Props> {
|
||||
@observable isScrolled = false;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
updateIsScrolled = () => {
|
||||
this.isScrolled = window.scrollY > 75;
|
||||
};
|
||||
|
||||
handleScroll = throttle(this.updateIsScrolled, 50);
|
||||
|
||||
handleNewDocument = () => {
|
||||
this.props.history.push(documentNewUrl(this.props.document));
|
||||
};
|
||||
|
||||
handleEdit = () => {
|
||||
this.props.history.push(documentEditUrl(this.props.document));
|
||||
};
|
||||
|
||||
handleSave = () => {
|
||||
this.props.onSave({ done: true });
|
||||
};
|
||||
|
||||
handlePublish = () => {
|
||||
this.props.onSave({ done: true, publish: true });
|
||||
};
|
||||
|
||||
handleClickTitle = () => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
document,
|
||||
isEditing,
|
||||
isDraft,
|
||||
isPublishing,
|
||||
isSaving,
|
||||
savingIsDisabled,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Actions
|
||||
align="center"
|
||||
justify="space-between"
|
||||
readOnly={!isEditing}
|
||||
isCompact={this.isScrolled}
|
||||
>
|
||||
<Breadcrumb document={document} />
|
||||
<Title isHidden={!this.isScrolled} onClick={this.handleClickTitle}>
|
||||
{document.title}
|
||||
</Title>
|
||||
<Wrapper align="center" justify="flex-end">
|
||||
{!isDraft && !isEditing && <Collaborators document={document} />}
|
||||
{isSaving &&
|
||||
!isPublishing && (
|
||||
<Action>
|
||||
<Status>Saving…</Status>
|
||||
</Action>
|
||||
)}
|
||||
{isDraft && (
|
||||
<Action>
|
||||
<Link
|
||||
onClick={this.handlePublish}
|
||||
title="Publish document (Cmd+Enter)"
|
||||
disabled={savingIsDisabled}
|
||||
highlight
|
||||
>
|
||||
{isPublishing ? 'Publishing…' : 'Publish'}
|
||||
</Link>
|
||||
</Action>
|
||||
)}
|
||||
{isEditing && (
|
||||
<React.Fragment>
|
||||
<Action>
|
||||
<Link
|
||||
onClick={this.handleSave}
|
||||
title="Save changes (Cmd+Enter)"
|
||||
disabled={savingIsDisabled}
|
||||
isSaving={isSaving}
|
||||
highlight={!isDraft}
|
||||
>
|
||||
{isDraft ? 'Save Draft' : 'Done'}
|
||||
</Link>
|
||||
</Action>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Action>
|
||||
<Link onClick={this.handleEdit}>Edit</Link>
|
||||
</Action>
|
||||
)}
|
||||
{isEditing &&
|
||||
!isSaving &&
|
||||
document.hasPendingChanges && (
|
||||
<Action>
|
||||
<Link onClick={this.props.onDiscard}>Discard</Link>
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Action>
|
||||
<DocumentMenu document={document} showPrint />
|
||||
</Action>
|
||||
)}
|
||||
{!isEditing &&
|
||||
!isDraft && (
|
||||
<React.Fragment>
|
||||
<Separator />
|
||||
<Action>
|
||||
<a onClick={this.handleNewDocument}>
|
||||
<NewDocumentIcon />
|
||||
</a>
|
||||
</Action>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Wrapper>
|
||||
</Actions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Status = styled.div`
|
||||
color: ${props => props.theme.slate};
|
||||
`;
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
width: 100%;
|
||||
align-self: flex-end;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
width: 33.3%;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-bottom: 1px solid
|
||||
${props => (props.isCompact ? props.theme.smoke : 'transparent')};
|
||||
padding: 12px;
|
||||
transition: all 100ms ease-out;
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${breakpoint('tablet')`
|
||||
padding: ${props => (props.isCompact ? '12px' : `24px 24px 0`)};
|
||||
`};
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
opacity: ${props => (props.isHidden ? '0' : '1')};
|
||||
cursor: ${props => (props.isHidden ? 'default' : 'pointer')};
|
||||
display: none;
|
||||
width: 0;
|
||||
|
||||
${breakpoint('tablet')`
|
||||
display: block;
|
||||
width: 33.3%;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Link = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: ${props => (props.highlight ? 500 : 'inherit')};
|
||||
color: ${props =>
|
||||
props.highlight ? `${props.theme.primary} !important` : 'inherit'};
|
||||
opacity: ${props => (props.disabled ? 0.5 : 1)};
|
||||
pointer-events: ${props => (props.disabled ? 'none' : 'auto')};
|
||||
cursor: ${props => (props.disabled ? 'default' : 'pointer')};
|
||||
`;
|
||||
|
||||
export default Header;
|
||||
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { pulsate } from 'shared/styles/animations';
|
||||
import { color } from 'shared/styles/constants';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import Fade from 'components/Fade';
|
||||
|
||||
@@ -29,7 +28,7 @@ const LoadingPlaceholder = (props: Object) => {
|
||||
const Mask = styled(Flex)`
|
||||
height: ${props => (props.header ? 28 : 18)}px;
|
||||
margin-bottom: ${props => (props.header ? 32 : 14)}px;
|
||||
background-color: ${color.smoke};
|
||||
background-color: ${props => props.theme.smoke};
|
||||
animation: ${pulsate} 1.3s infinite;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentDelete from './DocumentDelete';
|
||||
export default DocumentDelete;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import DocumentShare from './DocumentShare';
|
||||
export default DocumentShare;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Drafts from './Drafts';
|
||||
export default Drafts;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import ErrorSuspended from './ErrorSuspended';
|
||||
export default ErrorSuspended;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Home from './Home';
|
||||
export default Home;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
||||
export default KeyboardShortcuts;
|
||||
@@ -1,13 +1,12 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { SearchIcon } from 'outline-icons';
|
||||
|
||||
import Flex from 'shared/components/Flex';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
onChange: string => *,
|
||||
theme: Object,
|
||||
};
|
||||
|
||||
class SearchField extends React.Component<Props> {
|
||||
@@ -31,7 +30,7 @@ class SearchField extends React.Component<Props> {
|
||||
<StyledIcon
|
||||
type="Search"
|
||||
size={46}
|
||||
color={color.slateLight}
|
||||
color={this.props.theme.slateLight}
|
||||
onClick={this.focusInput}
|
||||
/>
|
||||
<StyledInput
|
||||
@@ -56,16 +55,16 @@ const StyledInput = styled.input`
|
||||
border: 0;
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: ${color.slateLight};
|
||||
color: ${props => props.theme.slateLight};
|
||||
}
|
||||
:-moz-placeholder {
|
||||
color: ${color.slateLight};
|
||||
color: ${props => props.theme.slateLight};
|
||||
}
|
||||
::-moz-placeholder {
|
||||
color: ${color.slateLight};
|
||||
color: ${props => props.theme.slateLight};
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
color: ${color.slateLight};
|
||||
color: ${props => props.theme.slateLight};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -74,4 +73,4 @@ const StyledIcon = styled(SearchIcon)`
|
||||
top: 4px;
|
||||
`;
|
||||
|
||||
export default SearchField;
|
||||
export default withTheme(SearchField);
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color, size } from 'shared/styles/constants';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
@@ -120,7 +119,7 @@ class Details extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const ProfilePicture = styled(Flex)`
|
||||
margin-bottom: ${size.huge};
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const avatarStyles = `
|
||||
@@ -133,7 +132,7 @@ const AvatarContainer = styled(Flex)`
|
||||
${avatarStyles};
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px #dae1e9;
|
||||
background: ${color.white};
|
||||
background: ${props => props.theme.white};
|
||||
|
||||
div div {
|
||||
${avatarStyles};
|
||||
@@ -150,7 +149,7 @@ const AvatarContainer = styled(Flex)`
|
||||
&:hover div {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: ${color.white};
|
||||
color: ${props => props.theme.white};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
71
app/scenes/Settings/Export.js
Normal file
71
app/scenes/Settings/Export.js
Normal file
@@ -0,0 +1,71 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Button from 'components/Button';
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
collections: CollectionsStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Export extends React.Component<Props> {
|
||||
@observable isLoading: boolean = false;
|
||||
@observable isExporting: boolean = false;
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
this.isLoading = true;
|
||||
|
||||
const success = await this.props.collections.export();
|
||||
|
||||
if (success) {
|
||||
this.isExporting = true;
|
||||
this.props.ui.showToast('Export in progress…', 'success');
|
||||
}
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { auth } = this.props;
|
||||
if (!auth.user) return;
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Export Data" />
|
||||
<h1>Export Data</h1>
|
||||
<HelpText>
|
||||
Exporting your teams documents may take a little time depending on the
|
||||
size of your knowledgebase. Consider exporting a single document or
|
||||
collection instead.
|
||||
</HelpText>
|
||||
<HelpText>
|
||||
Still want to export everything in your wiki? We’ll put together a zip
|
||||
file of your collections and documents in Markdown format and email it
|
||||
to <strong>{auth.user.email}</strong>.
|
||||
</HelpText>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={this.handleSubmit}
|
||||
disabled={this.isLoading || this.isExporting}
|
||||
primary
|
||||
>
|
||||
{this.isExporting
|
||||
? 'Export Requested'
|
||||
: this.isLoading ? 'Requesting Export…' : 'Export All Data'}
|
||||
</Button>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('auth', 'ui', 'collections')(Export);
|
||||
@@ -32,9 +32,8 @@ class People extends React.Component<Props> {
|
||||
<PageTitle title="People" />
|
||||
<h1>People</h1>
|
||||
<HelpText>
|
||||
Everyone that has signed in to your Outline appears here. It's
|
||||
possible that there are other people who have access but haven't
|
||||
signed in yet.
|
||||
Everyone that has signed in to your Outline appear here. It’s possible
|
||||
that there are other people who have access but haven’t signed in yet.
|
||||
</HelpText>
|
||||
|
||||
<List>
|
||||
@@ -42,7 +41,7 @@ class People extends React.Component<Props> {
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
isCurrentUser={currentUser.id === user.id}
|
||||
showMenu={!!currentUser.isAdmin && currentUser.id !== user.id}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color, size } from 'shared/styles/constants';
|
||||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
@@ -12,6 +11,7 @@ import Input, { LabelText } from 'components/Input';
|
||||
import Button from 'components/Button';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import UserDelete from 'scenes/UserDelete';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
type Props = {
|
||||
@@ -26,6 +26,7 @@ class Profile extends React.Component<Props> {
|
||||
|
||||
@observable name: string;
|
||||
@observable avatarUrl: ?string;
|
||||
@observable showDeleteModal: boolean = false;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.auth.user) {
|
||||
@@ -59,6 +60,10 @@ class Profile extends React.Component<Props> {
|
||||
this.props.ui.showToast(error || 'Unable to upload new avatar');
|
||||
};
|
||||
|
||||
toggleDeleteAccount = () => {
|
||||
this.showDeleteModal = !this.showDeleteModal;
|
||||
};
|
||||
|
||||
get isValid() {
|
||||
return this.form && this.form.checkValidity();
|
||||
}
|
||||
@@ -98,13 +103,30 @@ class Profile extends React.Component<Props> {
|
||||
{isSaving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<DangerZone>
|
||||
<LabelText>Delete Account</LabelText>
|
||||
<p>
|
||||
You may delete your account at any time, note that this is
|
||||
unrecoverable.{' '}
|
||||
<a onClick={this.toggleDeleteAccount}>Delete account</a>.
|
||||
</p>
|
||||
</DangerZone>
|
||||
{this.showDeleteModal && (
|
||||
<UserDelete onRequestClose={this.toggleDeleteAccount} />
|
||||
)}
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const DangerZone = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
`;
|
||||
|
||||
const ProfilePicture = styled(Flex)`
|
||||
margin-bottom: ${size.huge};
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const avatarStyles = `
|
||||
@@ -132,7 +154,7 @@ const AvatarContainer = styled(Flex)`
|
||||
&:hover div {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: ${color.white};
|
||||
color: ${props => props.theme.white};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||
import ShareMenu from 'menus/ShareMenu';
|
||||
import ListItem from 'components/List/Item';
|
||||
import Time from 'shared/components/Time';
|
||||
import type { Share } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
@@ -16,11 +16,8 @@ const ShareListItem = ({ share }: Props) => {
|
||||
title={share.documentTitle}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
Shared{' '}
|
||||
<time dateTime={share.createdAt}>
|
||||
{distanceInWordsToNow(new Date(share.createdAt))}
|
||||
</time>{' '}
|
||||
ago by {share.createdBy.name}
|
||||
Shared <Time dateTime={share.createdAt} /> ago by{' '}
|
||||
{share.createdBy.name}
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={<ShareMenu share={share} />}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { inject } from 'mobx-react';
|
||||
import { slackAuth } from 'shared/utils/routeHelpers';
|
||||
import Button from 'components/Button';
|
||||
import SlackLogo from 'shared/components/SlackLogo';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import Button from 'components/Button';
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
scopes?: string[],
|
||||
redirectUri?: string,
|
||||
redirectUri: string,
|
||||
state: string,
|
||||
label?: string,
|
||||
};
|
||||
|
||||
function SlackButton({ auth, state, label, scopes, redirectUri }: Props) {
|
||||
function SlackButton({ state, scopes, redirectUri, label }: Props) {
|
||||
const handleClick = () =>
|
||||
(window.location.href = slackAuth(state, scopes, redirectUri));
|
||||
|
||||
@@ -36,4 +33,4 @@ const SpacedSlackLogo = styled(SlackLogo)`
|
||||
padding-right: 4px;
|
||||
`;
|
||||
|
||||
export default inject('auth')(SlackButton);
|
||||
export default SlackButton;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'shared/styles/constants';
|
||||
|
||||
import UserMenu from 'menus/UserMenu';
|
||||
import Avatar from 'components/Avatar';
|
||||
import ListItem from 'components/List/Item';
|
||||
import Time from 'shared/components/Time';
|
||||
import type { User } from '../../../types';
|
||||
|
||||
type Props = {
|
||||
user: User,
|
||||
isCurrentUser: boolean,
|
||||
showMenu: boolean,
|
||||
};
|
||||
|
||||
const UserListItem = ({ user, isCurrentUser }: Props) => {
|
||||
const UserListItem = ({ user, showMenu }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
key={user.id}
|
||||
@@ -21,12 +21,13 @@ const UserListItem = ({ user, isCurrentUser }: Props) => {
|
||||
image={<Avatar src={user.avatarUrl} size={40} />}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{user.username ? user.username : user.email}
|
||||
{user.email ? `${user.email} · ` : undefined}
|
||||
Joined <Time dateTime={user.createdAt} /> ago
|
||||
{user.isAdmin && <Badge admin={user.isAdmin}>Admin</Badge>}
|
||||
{user.isSuspended && <Badge>Suspended</Badge>}
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={isCurrentUser ? undefined : <UserMenu user={user} />}
|
||||
actions={showMenu ? <UserMenu user={user} /> : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -34,8 +35,9 @@ const UserListItem = ({ user, isCurrentUser }: Props) => {
|
||||
const Badge = styled.span`
|
||||
margin-left: 10px;
|
||||
padding: 2px 6px 3px;
|
||||
background-color: ${({ admin }) => (admin ? color.primary : color.smokeDark)};
|
||||
color: ${({ admin }) => (admin ? color.white : color.text)};
|
||||
background-color: ${({ admin, theme }) =>
|
||||
admin ? theme.primary : theme.smokeDark};
|
||||
color: ${({ admin, theme }) => (admin ? theme.white : theme.text)};
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import Starred from './Starred';
|
||||
export default Starred;
|
||||
62
app/scenes/UserDelete.js
Normal file
62
app/scenes/UserDelete.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import Button from 'components/Button';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Modal from 'components/Modal';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
onRequestClose: () => *,
|
||||
};
|
||||
|
||||
@observer
|
||||
class UserDelete extends React.Component<Props> {
|
||||
@observable isDeleting: boolean;
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||
ev.preventDefault();
|
||||
this.isDeleting = true;
|
||||
|
||||
try {
|
||||
const success = await this.props.auth.deleteUser();
|
||||
|
||||
if (success) {
|
||||
this.props.auth.logout();
|
||||
}
|
||||
} finally {
|
||||
this.isDeleting = false;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { auth, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<Modal isOpen title="Delete Account" {...rest}>
|
||||
<Flex column>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<HelpText>
|
||||
Are you sure? Deleting your account will destory identifying data
|
||||
associated with your user and cannot be undone. You will be
|
||||
immediately logged out of Outline and all your API tokens will be
|
||||
revoked.
|
||||
</HelpText>
|
||||
<HelpText>
|
||||
<strong>Note:</strong> Signing back in will cause a new account to
|
||||
be automatically reprovisioned.
|
||||
</HelpText>
|
||||
<Button type="submit" danger>
|
||||
{this.isDeleting ? 'Deleting…' : 'Delete My Account'}
|
||||
</Button>
|
||||
</form>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('auth')(UserDelete);
|
||||
@@ -50,6 +50,17 @@ class AuthStore {
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
deleteUser = async () => {
|
||||
await client.post(`/user.delete`, { confirmation: true });
|
||||
|
||||
runInAction('AuthStore#updateUser', () => {
|
||||
this.user = null;
|
||||
this.team = null;
|
||||
this.token = null;
|
||||
});
|
||||
};
|
||||
|
||||
@action
|
||||
updateUser = async (params: { name: string, avatarUrl: ?string }) => {
|
||||
this.isSaving = true;
|
||||
|
||||
@@ -137,6 +137,16 @@ class CollectionsStore extends BaseStore {
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
export = async () => {
|
||||
try {
|
||||
await client.post('/collections.exportAll');
|
||||
return true;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
add = (collection: Collection): void => {
|
||||
this.data.set(collection.id, collection);
|
||||
|
||||
@@ -217,7 +217,7 @@ class DocumentsStore extends BaseStore {
|
||||
|
||||
if (res && res.data) {
|
||||
const duped = res.data;
|
||||
this.emit('documents.create', duped);
|
||||
this.emit('documents.create', new Document(duped));
|
||||
this.emit('documents.publish', {
|
||||
id: duped.id,
|
||||
collectionId: duped.collection.id,
|
||||
@@ -255,7 +255,7 @@ class DocumentsStore extends BaseStore {
|
||||
this.remove(data.id);
|
||||
});
|
||||
this.on('documents.create', (data: Document) => {
|
||||
this.add(new Document(data));
|
||||
this.add(data);
|
||||
});
|
||||
this.on('documents.duplicate', (data: Document) => {
|
||||
this.duplicate(data);
|
||||
|
||||
@@ -38,7 +38,7 @@ class SharesStore {
|
||||
@action
|
||||
revoke = async (share: Share) => {
|
||||
try {
|
||||
await client.post('/shares.delete', { id: share.id });
|
||||
await client.post('/shares.revoke', { id: share.id });
|
||||
runInAction('revoke', () => {
|
||||
this.data.delete(share.id);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ export type User = {
|
||||
username: string,
|
||||
isAdmin?: boolean,
|
||||
isSuspended?: boolean,
|
||||
createdAt: string,
|
||||
};
|
||||
|
||||
export type Toast = {
|
||||
@@ -36,11 +37,11 @@ export type NavigationNode = {
|
||||
id: string,
|
||||
title: string,
|
||||
url: string,
|
||||
children: Array<NavigationNode>,
|
||||
children: NavigationNode[],
|
||||
};
|
||||
|
||||
export type Document = {
|
||||
collaborators: Array<User>,
|
||||
collaborators: User[],
|
||||
collection: Object,
|
||||
createdAt: string,
|
||||
createdBy: User,
|
||||
|
||||
24
app/utils/emoji.js
Normal file
24
app/utils/emoji.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
|
||||
export function toCodePoint(unicodeSurrogates, sep) {
|
||||
var r = [],
|
||||
c = 0,
|
||||
p = 0,
|
||||
i = 0;
|
||||
while (i < unicodeSurrogates.length) {
|
||||
c = unicodeSurrogates.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xd800) << 10) + (c - 0xdc00)).toString(16));
|
||||
p = 0;
|
||||
} else if (0xd800 <= c && c <= 0xdbff) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
return r.join(sep || '-');
|
||||
}
|
||||
|
||||
export function emojiToUrl(string: text) {
|
||||
return `https://twemoji.maxcdn.com/2/72x72/${toCodePoint(string)}.png`;
|
||||
}
|
||||
37
flow-typed/npm/bcrypt_v1.x.x.js
vendored
37
flow-typed/npm/bcrypt_v1.x.x.js
vendored
@@ -1,37 +0,0 @@
|
||||
// flow-typed signature: 96d9e6596558a201899e45822d93e38d
|
||||
// flow-typed version: da30fe6876/bcrypt_v1.x.x/flow_>=v0.25.x
|
||||
|
||||
declare module bcrypt {
|
||||
declare function genSaltSync(rounds?: number): string;
|
||||
declare function genSalt(rounds: number): Promise<string>;
|
||||
declare function genSalt(): Promise<string>;
|
||||
declare function genSalt(callback: (err: Error, salt: string) => void): void;
|
||||
declare function genSalt(
|
||||
rounds: number,
|
||||
callback: (err: Error, salt: string) => void
|
||||
): void;
|
||||
declare function hashSync(data: string, salt: string): string;
|
||||
declare function hashSync(data: string, rounds: number): string;
|
||||
declare function hash(
|
||||
data: string,
|
||||
saltOrRounds: string | number
|
||||
): Promise<string>;
|
||||
declare function hash(
|
||||
data: string,
|
||||
rounds: number,
|
||||
callback: (err: Error, encrypted: string) => void
|
||||
): void;
|
||||
declare function hash(
|
||||
data: string,
|
||||
salt: string,
|
||||
callback: (err: Error, encrypted: string) => void
|
||||
): void;
|
||||
declare function compareSync(data: string, encrypted: string): boolean;
|
||||
declare function compare(data: string, encrypted: string): Promise<boolean>;
|
||||
declare function compare(
|
||||
data: string,
|
||||
encrypted: string,
|
||||
callback: (err: Error, same: boolean) => void
|
||||
): void;
|
||||
declare function getRounds(encrypted: string): number;
|
||||
}
|
||||
@@ -82,7 +82,6 @@
|
||||
"babel-preset-react": "6.11.1",
|
||||
"babel-preset-react-hmre": "1.1.1",
|
||||
"babel-regenerator-runtime": "6.5.0",
|
||||
"bcrypt": "1.0.3",
|
||||
"boundless-arrow-key-navigation": "^1.0.4",
|
||||
"boundless-popover": "^1.0.4",
|
||||
"bugsnag": "^1.7.0",
|
||||
@@ -112,6 +111,7 @@
|
||||
"js-search": "^1.4.2",
|
||||
"json-loader": "0.5.4",
|
||||
"jsonwebtoken": "7.0.1",
|
||||
"jszip": "3.1.5",
|
||||
"koa": "^2.2.0",
|
||||
"koa-bodyparser": "4.2.0",
|
||||
"koa-compress": "2.0.0",
|
||||
@@ -134,7 +134,7 @@
|
||||
"nodemailer": "^4.4.0",
|
||||
"normalize.css": "^7.0.0",
|
||||
"normalizr": "2.0.1",
|
||||
"outline-icons": "^1.2.0",
|
||||
"outline-icons": "^1.3.2",
|
||||
"oy-vey": "^0.10.0",
|
||||
"pg": "^6.1.5",
|
||||
"pg-hstore": "2.3.2",
|
||||
@@ -157,7 +157,7 @@
|
||||
"react-waypoint": "^7.3.1",
|
||||
"redis": "^2.6.2",
|
||||
"redis-lock": "^0.1.0",
|
||||
"rich-markdown-editor": "1.2.0",
|
||||
"rich-markdown-editor": "2.0.4",
|
||||
"safestart": "1.1.0",
|
||||
"sequelize": "4.28.6",
|
||||
"sequelize-cli": "^2.7.0",
|
||||
@@ -170,6 +170,7 @@
|
||||
"styled-components-breakpoint": "^1.0.1",
|
||||
"styled-components-grid": "^1.0.0-preview.15",
|
||||
"styled-normalize": "^2.2.1",
|
||||
"tmp": "0.0.33",
|
||||
"uglifyjs-webpack-plugin": "1.2.5",
|
||||
"url-loader": "^0.6.2",
|
||||
"uuid": "2.0.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 325 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 569 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user