Compare commits

..

19 Commits

Author SHA1 Message Date
Tom Moor e8168b52a2 chore: Cleanup related unused methods in UiStore 2020-08-11 22:23:23 -07:00
Tom Moor bfe10b1df0 refactor: Finally remove centralized Modals component 2020-08-11 22:20:13 -07:00
Tom Moor 810dc5a061 feat: Clicking the last updated time should open document history sidebar
Ref #1285
2020-08-11 21:01:03 -07:00
ktfth 7abe375b3e refactor: Removed unusued index on the onSearchLink (#1420)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2020-08-11 19:59:11 -07:00
Tom Moor 63371d8f5b flow 2020-08-11 18:59:57 -07:00
Tom Moor 6e61df0729 fix: Improved loading jank fix, new DelayedMount component 2020-08-10 21:30:12 -07:00
Tom Moor 5ddc4000d0 fixes: Strange scroll behavior on long collection descriptions
closes #1391
2020-08-10 16:23:55 -07:00
Tom Moor 48b61559cc fixes: JS error when attempting to show toast messages from collection description editor 2020-08-10 16:04:23 -07:00
Tom Moor 0cac5cfe51 fix: Prevent reload loop with error on editor load 2020-08-10 15:52:45 -07:00
Tom Moor e9ce80a3aa fixes: Case where websocket will not reconnect
closes #1384
2020-08-09 23:25:27 -07:00
Tom Moor 07d488c826 fix: GitHub Gist embed reliability, closes #1400 2020-08-09 21:53:57 -07:00
Tom Moor e2bd03494d chore: Update syntax, improve more typing (#1439)
* chore: <React.Fragment> to <>

* flow types
2020-08-09 09:48:04 -07:00
Tom Moor ead55442e0 flow: Restore lesser flowtype for styled-components
The current flow-typed def requires an insane amount of manual typing that just doesnt
make any sense. Restoring the old definition for now:
https://github.com/flow-typed/flow-typed/issues/3766
2020-08-08 23:41:02 -07:00
Tom Moor 449dc55aaa chore: Upgrade Babel, Jest, Eslint (#1437)
* chore: Upgrade Prettier 1.8 -> 2.0

* chore: Upgrade Babel 6 -> 7

* chore: Upgrade eslint plugins

* chore: Add eslint import/order rules

* chore: Update flow-typed deps
2020-08-08 22:53:59 -07:00
Tom Moor e312b264a6 chore: Upgrade Prettier 1.8 -> 2.0 (#1436) 2020-08-08 18:53:11 -07:00
Tom Moor 68dcb4de5f fix: Catch expected error when shares.info returns 404 2020-08-08 17:55:21 -07:00
Tom Moor d2b9a5c03f fix: Various React errors in console 2020-08-08 17:51:40 -07:00
Tom Moor 1b023fb6d7 fix: Remove flash of loading state for document lists 2020-08-08 17:39:30 -07:00
Tom Moor afe4553a7e chore: Resolve 2 open security alerts 2020-08-08 17:35:42 -07:00
369 changed files with 18384 additions and 14408 deletions
+25 -14
View File
@@ -1,18 +1,29 @@
{
"presets": ["react", "env"],
"presets": [
"@babel/preset-react",
"@babel/preset-flow",
[
"@babel/preset-env",
{
"corejs": {
"version": "2",
"proposals": true
},
"useBuiltIns": "usage",
}
]
],
"plugins": [
"lodash",
"styled-components",
"transform-decorators-legacy",
"transform-es2015-destructuring",
"transform-object-rest-spread",
"transform-regenerator",
"transform-class-properties",
"syntax-dynamic-import"
],
"env": {
"development": {
"presets": ["react-hmre"]
}
}
}
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-transform-destructuring",
"@babel/plugin-transform-regenerator",
"transform-class-properties"
]
}
+41 -2
View File
@@ -14,6 +14,46 @@
"eqeqeq": 2,
"no-unused-vars": 2,
"no-mixed-operators": "off",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"pathGroups": [
{
"pattern": "shared/**",
"group": "external",
"position": "after"
},
{
"pattern": "stores",
"group": "external",
"position": "after"
},
{
"pattern": "stores/**",
"group": "external",
"position": "after"
},
{
"pattern": "models/**",
"group": "external",
"position": "after"
},
{
"pattern": "scenes/**",
"group": "external",
"position": "after"
},
{
"pattern": "components/**",
"group": "external",
"position": "after"
}
]
}
],
"flowtype/require-valid-file-annotation": [
2,
"always",
@@ -41,8 +81,7 @@
"react": {
"createClass": "createReactClass",
"pragma": "React",
"version": "detect",
"flowVersion": "0.86"
"version": "detect"
},
"import/resolver": {
"node": {
+4 -4
View File
@@ -12,7 +12,7 @@ export const Action = styled(Flex)`
flex-shrink: 0;
a {
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
height: 24px;
}
@@ -26,7 +26,7 @@ export const Separator = styled.div`
margin-left: 12px;
width: 1px;
height: 28px;
background: ${props => props.theme.divider};
background: ${(props) => props.theme.divider};
`;
const Actions = styled(Flex)`
@@ -35,8 +35,8 @@ const Actions = styled(Flex)`
right: 0;
left: 0;
border-radius: 3px;
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 12px;
-webkit-backdrop-filter: blur(20px);
+1 -1
View File
@@ -14,7 +14,7 @@ export default class Analytics extends React.Component<Props> {
// standard Google Analytics script
window.ga =
window.ga ||
function() {
function () {
// $FlowIssue
(ga.q = ga.q || []).push(arguments);
};
+2 -2
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { isCustomSubdomain } from "shared/utils/domains";
import AuthStore from "stores/AuthStore";
import LoadingIndicator from "components/LoadingIndicator";
import { isCustomSubdomain } from "shared/utils/domains";
import env from "env";
type Props = {
+7 -7
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import placeholder from "./placeholder.png";
type Props = {
@@ -48,8 +48,8 @@ const IconWrapper = styled.div`
position: absolute;
bottom: -2px;
right: -2px;
background: ${props => props.theme.primary};
border: 2px solid ${props => props.theme.background};
background: ${(props) => props.theme.primary};
border: 2px solid ${(props) => props.theme.background};
border-radius: 100%;
width: 20px;
height: 20px;
@@ -57,10 +57,10 @@ const IconWrapper = styled.div`
const CircleImg = styled.img`
display: block;
width: ${props => props.size}px;
height: ${props => props.size}px;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
border: 2px solid ${props => props.theme.background};
border: 2px solid ${(props) => props.theme.background};
flex-shrink: 0;
`;
+11 -9
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { EditIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import Avatar from "components/Avatar";
import Tooltip from "components/Tooltip";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
import { EditIcon } from "outline-icons";
import Avatar from "components/Avatar";
import Tooltip from "components/Tooltip";
type Props = {
user: User,
@@ -40,14 +40,16 @@ class AvatarWithPresence extends React.Component<Props> {
} = this.props;
return (
<React.Fragment>
<>
<Tooltip
tooltip={
<Centered>
<strong>{user.name}</strong> {isCurrentUser && "(You)"}
<br />
{isPresent
? isEditing ? "currently editing" : "currently viewing"
? isEditing
? "currently editing"
: "currently viewing"
: `viewed ${distanceInWordsToNow(new Date(lastViewedAt))} ago`}
</Centered>
}
@@ -67,7 +69,7 @@ class AvatarWithPresence extends React.Component<Props> {
isOpen={this.isOpen}
onRequestClose={this.handleCloseProfile}
/>
</React.Fragment>
</>
);
}
}
@@ -77,7 +79,7 @@ const Centered = styled.div`
`;
const AvatarWrapper = styled.div`
opacity: ${props => (props.isPresent ? 1 : 0.5)};
opacity: ${(props) => (props.isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
`;
+5 -4
View File
@@ -11,7 +11,8 @@ type Props = {
function Branding({ href = env.URL }: Props) {
return (
<Link href={href}>
<OutlineLogo size={16} />&nbsp;Outline
<OutlineLogo size={16} />
&nbsp;Outline
</Link>
);
}
@@ -25,17 +26,17 @@ const Link = styled.a`
font-size: 14px;
text-decoration: none;
border-top-right-radius: 2px;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
display: flex;
align-items: center;
padding: 16px;
svg {
fill: ${props => props.theme.text};
fill: ${(props) => props.theme.text};
}
&:hover {
background: ${props => props.theme.sidebarBackground};
background: ${(props) => props.theme.sidebarBackground};
}
`;
+29 -26
View File
@@ -1,9 +1,5 @@
// @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 {
PadlockIcon,
GoToIcon,
@@ -11,13 +7,17 @@ import {
ShapesIcon,
EditIcon,
} from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import Document from "models/Document";
import CollectionsStore from "stores/CollectionsStore";
import { collectionUrl } from "utils/routeHelpers";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
import BreadcrumbMenu from "./BreadcrumbMenu";
import CollectionIcon from "components/CollectionIcon";
import { collectionUrl } from "utils/routeHelpers";
type Props = {
document: Document,
@@ -33,20 +33,20 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
if (onlyText === true) {
return (
<React.Fragment>
<>
{collection.private && (
<React.Fragment>
<>
<SmallPadlockIcon color="currentColor" size={16} />{" "}
</React.Fragment>
</>
)}
{collection.name}
{path.map(n => (
{path.map((n) => (
<React.Fragment key={n.id}>
<SmallSlash />
{n.title}
</React.Fragment>
))}
</React.Fragment>
</>
);
}
@@ -59,39 +59,42 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<React.Fragment>
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />&nbsp;
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
{isDraft && (
<React.Fragment>
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />&nbsp;
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />&nbsp;
<CollectionIcon collection={collection} expanded />
&nbsp;
<span>{collection.name}</span>
</CollectionName>
{isNestedDocument && (
<React.Fragment>
<>
<Slash /> <BreadcrumbMenu label={<Overflow />} path={menuPath} />
</React.Fragment>
</>
)}
{lastPath && (
<React.Fragment>
<>
<Slash />{" "}
<Crumb to={lastPath.url} title={lastPath.title}>
{lastPath.title}
</Crumb>
</React.Fragment>
</>
)}
</Wrapper>
);
@@ -119,7 +122,7 @@ const SmallSlash = styled(GoToIcon)`
export const Slash = styled(GoToIcon)`
flex-shrink: 0;
fill: ${props => props.theme.divider};
fill: ${(props) => props.theme.divider};
`;
const Overflow = styled(MoreIcon)`
@@ -134,7 +137,7 @@ const Overflow = styled(MoreIcon)`
`;
const Crumb = styled(Link)`
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-size: 15px;
height: 24px;
text-overflow: ellipsis;
@@ -149,7 +152,7 @@ const Crumb = styled(Link)`
const CollectionName = styled(Link)`
display: flex;
flex-shrink: 0;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-size: 15px;
font-weight: 500;
white-space: nowrap;
+1 -1
View File
@@ -14,7 +14,7 @@ export default class BreadcrumbMenu extends React.Component<Props> {
return (
<DropdownMenu label={this.props.label} position="center">
{path.map(item => (
{path.map((item) => (
<DropdownMenuItem as={Link} to={item.url} key={item.id}>
{item.title}
</DropdownMenuItem>
+20 -20
View File
@@ -1,17 +1,17 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import { darken, lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
import { darken, lighten } from "polished";
import { ExpandedIcon } from "outline-icons";
const RealButton = styled.button`
display: ${props => (props.fullwidth ? "block" : "inline-block")};
width: ${props => (props.fullwidth ? "100%" : "auto")};
display: ${(props) => (props.fullwidth ? "block" : "inline-block")};
width: ${(props) => (props.fullwidth ? "100%" : "auto")};
margin: 0;
padding: 0;
border: 0;
background: ${props => props.theme.buttonBackground};
color: ${props => props.theme.buttonText};
background: ${(props) => props.theme.buttonBackground};
color: ${(props) => props.theme.buttonText};
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px;
border-radius: 4px;
font-size: 14px;
@@ -24,7 +24,7 @@ const RealButton = styled.button`
user-select: none;
svg {
fill: ${props => props.iconColor || props.theme.buttonText};
fill: ${(props) => props.iconColor || props.theme.buttonText};
}
&::-moz-focus-inner {
@@ -33,12 +33,12 @@ const RealButton = styled.button`
}
&:hover {
background: ${props => darken(0.05, props.theme.buttonBackground)};
background: ${(props) => darken(0.05, props.theme.buttonBackground)};
}
&:focus {
transition-duration: 0.05s;
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
@@ -46,10 +46,10 @@ const RealButton = styled.button`
&:disabled {
cursor: default;
pointer-events: none;
color: ${props => props.theme.white50};
color: ${(props) => props.theme.white50};
}
${props =>
${(props) =>
props.neutral &&
`
background: ${props.theme.buttonNeutralBackground};
@@ -80,9 +80,9 @@ const RealButton = styled.button`
&:disabled {
color: ${props.theme.textTertiary};
}
`} ${props =>
props.danger &&
`
`} ${(props) =>
props.danger &&
`
background: ${props.theme.danger};
color: ${props.theme.white};
@@ -103,20 +103,20 @@ const Label = styled.span`
white-space: nowrap;
text-overflow: ellipsis;
${props => props.hasIcon && "padding-left: 4px;"};
${(props) => props.hasIcon && "padding-left: 4px;"};
`;
export const Inner = styled.span`
display: flex;
padding: 0 8px;
padding-right: ${props => (props.disclosure ? 2 : 8)}px;
line-height: ${props => (props.hasIcon ? 24 : 32)}px;
padding-right: ${(props) => (props.disclosure ? 2 : 8)}px;
line-height: ${(props) => (props.hasIcon ? 24 : 32)}px;
justify-content: center;
align-items: center;
min-height: 30px;
${props => props.hasIcon && props.hasText && "padding-left: 4px;"};
${props => props.hasIcon && !props.hasText && "padding: 0 4px;"};
${(props) => props.hasIcon && props.hasText && "padding-left: 4px;"};
${(props) => props.hasIcon && !props.hasText && "padding: 0 4px;"};
`;
export type Props = {
@@ -155,6 +155,6 @@ function Button({
);
}
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Button>((props, ref) => (
<Button {...props} innerRef={ref} />
));
+6 -5
View File
@@ -10,18 +10,19 @@ export type Props = {
labelHidden?: boolean,
className?: string,
note?: string,
short?: boolean,
small?: boolean,
};
const LabelText = styled.span`
font-weight: 500;
margin-left: ${props => (props.small ? "6px" : "10px")};
${props => (props.small ? `color: ${props.theme.textSecondary}` : "")};
margin-left: ${(props) => (props.small ? "6px" : "10px")};
${(props) => (props.small ? `color: ${props.theme.textSecondary}` : "")};
`;
const Wrapper = styled.div`
padding-bottom: 8px;
${props => (props.small ? "font-size: 14px" : "")};
${(props) => (props.small ? "font-size: 14px" : "")};
`;
const Label = styled.label`
@@ -42,7 +43,7 @@ export default function Checkbox({
const wrappedLabel = <LabelText small={small}>{label}</LabelText>;
return (
<React.Fragment>
<>
<Wrapper small={small}>
<Label>
<input type="checkbox" {...rest} />
@@ -55,6 +56,6 @@ export default function Checkbox({
</Label>
{note && <HelpText small>{note}</HelpText>}
</Wrapper>
</React.Fragment>
</>
);
}
+12 -12
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import { sortBy, keyBy } from "lodash";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
import { AvatarWithPresence } from "components/Avatar";
import Facepile from "components/Facepile";
import Document from "models/Document";
import ViewsStore from "stores/ViewsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
type Props = {
views: ViewsStore,
@@ -32,27 +32,27 @@ class Collaborators extends React.Component<Props> {
const documentViews = views.inDocument(document.id);
const presentIds = documentPresence.map(p => p.userId);
const presentIds = documentPresence.map((p) => p.userId);
const editingIds = documentPresence
.filter(p => p.isEditing)
.map(p => p.userId);
.filter((p) => p.isEditing)
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
const mostRecentViewers = sortBy(
documentViews.slice(0, MAX_AVATAR_DISPLAY),
view => {
(view) => {
return presentIds.includes(view.user.id);
}
);
const viewersKeyedByUserId = keyBy(mostRecentViewers, v => v.user.id);
const viewersKeyedByUserId = keyBy(mostRecentViewers, (v) => v.user.id);
const overflow = documentViews.length - mostRecentViewers.length;
return (
<Facepile
users={mostRecentViewers.map(v => v.user)}
users={mostRecentViewers.map((v) => v.user)}
overflow={overflow}
renderAvatar={user => {
renderAvatar={(user) => {
const isPresent = presentIds.includes(user.id);
const isEditing = editingIds.includes(user.id);
const { lastViewedAt } = viewersKeyedByUserId[user.id];
+3 -3
View File
@@ -1,11 +1,11 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import { getLuminance } from "polished";
import { PrivateCollectionIcon, CollectionIcon } from "outline-icons";
import { getLuminance } from "polished";
import * as React from "react";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import { icons } from "components/IconPicker";
import UiStore from "stores/UiStore";
type Props = {
collection: Collection,
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import copy from "copy-to-clipboard";
import * as React from "react";
type Props = {
text: string,
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
type Props = {
delay?: number,
children: React.Node,
};
export default function DelayedMount({ delay = 150, children }: Props) {
const [isShowing, setShowing] = React.useState(false);
React.useEffect(() => {
const timeout = setTimeout(() => setShowing(true), delay);
return () => {
clearTimeout(timeout);
};
}, []);
if (!isShowing) {
return null;
}
return children;
}
@@ -1,11 +1,11 @@
// @flow
import * as React from "react";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { type RouterHistory, type Match } from "react-router-dom";
import styled from "styled-components";
import { Waypoint } from "react-waypoint";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import styled from "styled-components";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
@@ -133,16 +133,16 @@ const Wrapper = styled(Flex)`
top: 0;
right: 0;
z-index: 1;
min-width: ${props => props.theme.sidebarWidth};
min-width: ${(props) => props.theme.sidebarWidth};
height: 100%;
overflow-y: auto;
overscroll-behavior: none;
`;
const Sidebar = styled(Flex)`
background: ${props => props.theme.background};
min-width: ${props => props.theme.sidebarWidth};
border-left: 1px solid ${props => props.theme.divider};
background: ${(props) => props.theme.background};
min-width: ${(props) => props.theme.sidebarWidth};
border-left: 1px solid ${(props) => props.theme.divider};
z-index: 1;
`;
@@ -1,16 +1,16 @@
// @flow
import format from "date-fns/format";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import format from "date-fns/format";
import { MoreIcon } from "outline-icons";
import Flex from "components/Flex";
import Time from "components/Time";
import Avatar from "components/Avatar";
import RevisionMenu from "menus/RevisionMenu";
import Document from "models/Document";
import Revision from "models/Revision";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
import Time from "components/Time";
import RevisionMenu from "menus/RevisionMenu";
import { documentHistoryUrl } from "utils/routeHelpers";
@@ -66,7 +66,7 @@ const StyledRevisionMenu = styled(RevisionMenu)`
`;
const StyledNavLink = styled(NavLink)`
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
display: block;
padding: 8px 16px;
font-size: 15px;
+2 -2
View File
@@ -1,8 +1,8 @@
// @flow
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
type Props = {
documents: Document[],
@@ -17,7 +17,7 @@ export default function DocumentList({ limit, documents, ...rest }: Props) {
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
{items.map(document => (
{items.map((document) => (
<DocumentPreview key={document.id} document={document} {...rest} />
))}
</ArrowKeyNavigation>
+111 -27
View File
@@ -1,39 +1,123 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { inject } from "mobx-react";
import ViewsStore from "stores/ViewsStore";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import PublishingInfo from "components/PublishingInfo";
import Breadcrumb from "components/Breadcrumb";
import Flex from "components/Flex";
import Time from "components/Time";
type Props = {|
views: ViewsStore,
const Container = styled(Flex)`
color: ${(props) => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${(props) =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${(props) => (props.highlight ? "600" : "400")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
isDraft: boolean,
|};
children: React.Node,
to?: string,
};
function DocumentMeta({ views, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
function DocumentMeta({
auth,
collections,
showPublished,
showCollection,
document,
children,
to,
...rest
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
createdAt,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
// Currently the situation where this is true is rendering share links.
if (!updatedBy) {
return null;
}
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
return (
<Meta document={document}>
{totalViews && !isDraft ? (
<React.Fragment>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
</React.Fragment>
) : null}
</Meta>
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{to ? <Link to={to}>{content}</Link> : content}
{showCollection && collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
{children}
</Container>
);
}
const Meta = styled(PublishingInfo)`
margin: -12px 0 2em 0;
font-size: 14px;
@media print {
display: none;
}
`;
export default inject("views")(DocumentMeta);
export default inject("collections", "auth")(observer(DocumentMeta));
+48
View File
@@ -0,0 +1,48 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
import DocumentMeta from "components/DocumentMeta";
type Props = {|
views: ViewsStore,
document: Document,
isDraft: boolean,
to?: string,
|};
function DocumentMetaWithViews({ views, to, isDraft, document }: Props) {
const totalViews = views.countForDocument(document.id);
return (
<Meta document={document} to={to}>
{totalViews && !isDraft ? (
<>
&nbsp;&middot; Viewed{" "}
{totalViews === 1 ? "once" : `${totalViews} times`}
</>
) : null}
</Meta>
);
}
const Meta = styled(DocumentMeta)`
margin: -12px 0 2em 0;
font-size: 14px;
a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
@media print {
display: none;
}
`;
export default inject("views")(DocumentMetaWithViews);
@@ -1,20 +1,21 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { StarredIcon, PlusIcon } from "outline-icons";
import * as React from "react";
import { Link, withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
import Tooltip from "components/Tooltip";
import DocumentMeta from "components/DocumentMeta";
import Flex from "components/Flex";
import Highlight from "components/Highlight";
import PublishingInfo from "components/PublishingInfo";
import Tooltip from "components/Tooltip";
import DocumentMenu from "menus/DocumentMenu";
import Document from "models/Document";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
document: Document,
highlight?: ?string,
context?: ?string,
@@ -47,6 +48,19 @@ class DocumentPreview extends React.Component<Props> {
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
handleNewFromTemplate = (event) => {
event.preventDefault();
event.stopPropagation();
const { document } = this.props;
this.props.history.push(
newDocumentUrl(document.collectionId, {
templateId: document.id,
})
);
};
render() {
const {
document,
@@ -57,7 +71,6 @@ class DocumentPreview extends React.Component<Props> {
showTemplate,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
@@ -70,7 +83,6 @@ class DocumentPreview extends React.Component<Props> {
pathname: document.url,
state: { title: document.titleWithDefault },
}}
{...rest}
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
@@ -85,33 +97,27 @@ class DocumentPreview extends React.Component<Props> {
)}
</Actions>
)}
{document.isDraft &&
showDraft && (
<Tooltip
tooltip="Only visible to you"
delay={500}
placement="top"
>
<Badge>Draft</Badge>
</Tooltip>
)}
{document.isTemplate &&
showTemplate && <Badge primary>Template</Badge>}
{document.isDraft && showDraft && (
<Tooltip tooltip="Only visible to you" delay={500} placement="top">
<Badge>Draft</Badge>
</Tooltip>
)}
{document.isTemplate && showTemplate && (
<Badge primary>Template</Badge>
)}
<SecondaryActions>
{document.isTemplate &&
!document.isArchived &&
!document.isDeleted && (
<Button
as={Link}
to={newDocumentUrl(document.collectionId, {
templateId: document.id,
})}
onClick={this.handleNewFromTemplate}
icon={<PlusIcon />}
neutral
>
New doc
</Button>
)}&nbsp;
)}
&nbsp;
<DocumentMenu document={document} showPin={showPin} />
</SecondaryActions>
</Heading>
@@ -123,7 +129,7 @@ class DocumentPreview extends React.Component<Props> {
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
@@ -137,7 +143,7 @@ const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
<StarredIcon color={theme.text} {...props} />
))`
flex-shrink: 0;
opacity: ${props => (props.solid ? "1 !important" : 0)};
opacity: ${(props) => (props.solid ? "1 !important" : 0)};
transition: all 100ms ease-in-out;
&:hover {
@@ -173,7 +179,7 @@ const DocumentLink = styled(Link)`
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
${SecondaryActions} {
@@ -198,7 +204,7 @@ const Heading = styled.h3`
margin-bottom: 0.25em;
overflow: hidden;
white-space: nowrap;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
`;
@@ -216,10 +222,10 @@ const Title = styled(Highlight)`
const ResultContext = styled(Highlight)`
display: block;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
font-size: 14px;
margin-top: -0.25em;
margin-bottom: 0.25em;
`;
export default DocumentPreview;
export default withRouter(DocumentPreview);
+7 -7
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import invariant from "invariant";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import Dropzone from "react-dropzone";
import { withRouter, type RouterHistory, type Match } from "react-router-dom";
import { createGlobalStyle } from "styled-components";
import invariant from "invariant";
import importFile from "utils/importFile";
import Dropzone from "react-dropzone";
import DocumentsStore from "stores/DocumentsStore";
import LoadingIndicator from "components/LoadingIndicator";
import importFile from "utils/importFile";
const EMPTY_OBJECT = {};
let importingLock = false;
@@ -30,12 +30,12 @@ type Props = {
export const GlobalStyles = createGlobalStyle`
.activeDropZone {
border-radius: 4px;
background: ${props => props.theme.slateDark};
svg { fill: ${props => props.theme.white}; }
background: ${(props) => props.theme.slateDark};
svg { fill: ${(props) => props.theme.white}; }
}
.activeDropZone a {
color: ${props => props.theme.white} !important;
color: ${(props) => props.theme.white} !important;
}
`;
+12 -12
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import invariant from "invariant";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { PortalWithState } from "react-portal";
import { MoreIcon } from "outline-icons";
import { rgba } from "polished";
import * as React from "react";
import { PortalWithState } from "react-portal";
import styled from "styled-components";
import Flex from "components/Flex";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
let previousClosePortal;
@@ -161,7 +161,7 @@ class DropdownMenu extends React.Component<Props> {
closeOnEsc
>
{({ closePortal, openPortal, isOpen, portal }) => (
<React.Fragment>
<>
<Label
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
@@ -204,7 +204,7 @@ class DropdownMenu extends React.Component<Props> {
onClick={
typeof children === "function"
? undefined
: ev => {
: (ev) => {
ev.stopPropagation();
closePortal();
}
@@ -220,7 +220,7 @@ class DropdownMenu extends React.Component<Props> {
</Menu>
</Position>
)}
</React.Fragment>
</>
)}
</PortalWithState>
</div>
@@ -245,24 +245,24 @@ const Position = styled.div`
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
transform: ${props =>
transform: ${(props) =>
props.position === "center" ? "translateX(-50%)" : "initial"};
pointer-events: none;
`;
const Menu = styled.div`
animation: ${fadeAndScaleIn} 200ms ease;
transform-origin: ${props => (props.left !== undefined ? "25%" : "75%")} 0;
transform-origin: ${(props) => (props.left !== undefined ? "25%" : "75%")} 0;
backdrop-filter: blur(10px);
background: ${props => rgba(props.theme.menuBackground, 0.8)};
border: ${props =>
background: ${(props) => rgba(props.theme.menuBackground, 0.8)};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
border-radius: 2px;
padding: 0.5em 0;
min-width: 180px;
overflow: hidden;
overflow-y: auto;
box-shadow: ${props => props.theme.menuShadow};
box-shadow: ${(props) => props.theme.menuShadow};
pointer-events: all;
hr {
@@ -278,7 +278,7 @@ export const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { CheckmarkIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
type Props = {
@@ -26,11 +26,12 @@ const DropdownMenuItem = ({
{...rest}
>
{selected !== undefined && (
<React.Fragment>
<>
<CheckmarkIcon
color={selected === false ? "transparent" : undefined}
/>&nbsp;
</React.Fragment>
/>
&nbsp;
</>
)}
{children}
</MenuItem>
@@ -44,7 +45,7 @@ const MenuItem = styled.a`
width: 100%;
min-height: 32px;
color: ${props =>
color: ${(props) =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};
justify-content: left;
align-items: center;
@@ -57,10 +58,10 @@ const MenuItem = styled.a`
}
svg {
opacity: ${props => (props.disabled ? ".5" : 1)};
opacity: ${(props) => (props.disabled ? ".5" : 1)};
}
${props =>
${(props) =>
props.disabled
? "pointer-events: none;"
: `
+21 -21
View File
@@ -1,34 +1,32 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { lighten } from "polished";
import styled, { withTheme } from "styled-components";
import RichMarkdownEditor from "rich-markdown-editor";
import { uploadFile } from "utils/uploadFile";
import isInternalUrl from "utils/isInternalUrl";
import Tooltip from "components/Tooltip";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import isInternalUrl from "utils/isInternalUrl";
import { uploadFile } from "utils/uploadFile";
const EMPTY_ARRAY = [];
type Props = {
id: string,
id?: string,
defaultValue?: string,
readOnly?: boolean,
grow?: boolean,
disableEmbeds?: boolean,
history: RouterHistory,
forwardedRef: React.Ref<RichMarkdownEditor>,
ui: UiStore,
ui?: UiStore,
};
@observer
class Editor extends React.Component<Props> {
@observable redirectTo: ?string;
type PropsWithRef = Props & {
forwardedRef: React.Ref<RichMarkdownEditor>,
history: RouterHistory,
};
class Editor extends React.Component<PropsWithRef> {
onUploadImage = async (file: File) => {
const result = await uploadFile(file, { documentId: this.props.id });
return result.url;
@@ -62,7 +60,9 @@ class Editor extends React.Component<Props> {
};
onShowToast = (message: string) => {
this.props.ui.showToast(message);
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
render() {
@@ -81,11 +81,11 @@ class Editor extends React.Component<Props> {
}
const StyledEditor = styled(RichMarkdownEditor)`
flex-grow: ${props => (props.grow ? 1 : 0)};
flex-grow: ${(props) => (props.grow ? 1 : 0)};
justify-content: start;
> div {
transition: ${props => props.theme.backgroundTransition};
transition: ${(props) => props.theme.backgroundTransition};
}
.notice-block.tip,
@@ -95,13 +95,13 @@ const StyledEditor = styled(RichMarkdownEditor)`
p {
a {
color: ${props => props.theme.text};
border-bottom: 1px solid ${props => lighten(0.5, props.theme.text)};
color: ${(props) => props.theme.text};
border-bottom: 1px solid ${(props) => lighten(0.5, props.theme.text)};
text-decoration: none !important;
font-weight: 500;
&:hover {
border-bottom: 1px solid ${props => props.theme.text};
border-bottom: 1px solid ${(props) => props.theme.text};
text-decoration: none;
}
}
@@ -120,6 +120,6 @@ const Span = styled.span`
const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
));
+1 -1
View File
@@ -2,7 +2,7 @@
import styled from "styled-components";
const Empty = styled.p`
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
`;
export default Empty;
+9 -8
View File
@@ -1,13 +1,14 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { observer } from "mobx-react";
import { observable } from "mobx";
import HelpText from "components/HelpText";
import Button from "components/Button";
import CenteredContent from "components/CenteredContent";
import HelpText from "components/HelpText";
import PageTitle from "components/PageTitle";
import { githubIssuesUrl } from "../../shared/utils/routeHelpers";
import env from "env";
type Props = {
children: React.Node,
@@ -41,16 +42,16 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const isReported = !!window.Sentry;
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
return (
<CenteredContent>
<PageTitle title="Something Unexpected Happened" />
<h1>Something Unexpected Happened</h1>
<HelpText>
Sorry, an unrecoverable error occurred{isReported &&
" our engineers have been notified"}. Please try reloading the
page, it may have been a temporary glitch.
Sorry, an unrecoverable error occurred
{isReported && " our engineers have been notified"}. Please try
reloading the page, it may have been a temporary glitch.
</HelpText>
{this.showDetails && <Pre>{this.error.toString()}</Pre>}
<p>
@@ -73,7 +74,7 @@ class ErrorBoundary extends React.Component<Props> {
}
const Pre = styled.pre`
background: ${props => props.theme.smoke};
background: ${(props) => props.theme.smoke};
padding: 16px;
border-radius: 4px;
font-size: 12px;
+9 -9
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import Avatar from "components/Avatar";
import User from "models/User";
import Avatar from "components/Avatar";
import Flex from "components/Flex";
type Props = {
users: User[],
@@ -31,7 +31,7 @@ class Facepile extends React.Component<Props> {
<span>+{overflow}</span>
</More>
)}
{users.map(user => (
{users.map((user) => (
<AvatarWrapper key={user.id}>{renderAvatar(user)}</AvatarWrapper>
))}
</Avatars>
@@ -56,12 +56,12 @@ const More = styled.div`
flex-direction: column;
align-items: center;
justify-content: center;
min-width: ${props => props.size}px;
height: ${props => props.size}px;
min-width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 100%;
background: ${props => props.theme.slate};
color: ${props => props.theme.text};
border: 2px solid ${props => props.theme.background};
background: ${(props) => props.theme.slate};
color: ${(props) => props.theme.text};
border: 2px solid ${(props) => props.theme.background};
text-align: center;
font-size: 11px;
font-weight: 600;
+1 -1
View File
@@ -3,7 +3,7 @@ import styled from "styled-components";
import { fadeIn } from "shared/styles/animations";
const Fade = styled.span`
animation: ${fadeIn} ${props => props.timing || "250ms"} ease-in-out;
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
`;
export default Fade;
+14 -14
View File
@@ -1,17 +1,17 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
import Modal from "components/Modal";
import Flex from "components/Flex";
import Facepile from "components/Facepile";
import GroupMembers from "scenes/GroupMembers";
import ListItem from "components/List/Item";
import Group from "models/Group";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import GroupMembershipsStore from "stores/GroupMembershipsStore";
import CollectionGroupMembership from "models/CollectionGroupMembership";
import Group from "models/Group";
import GroupMembers from "scenes/GroupMembers";
import Facepile from "components/Facepile";
import Flex from "components/Flex";
import ListItem from "components/List/Item";
import Modal from "components/Modal";
type Props = {
group: Group,
@@ -41,20 +41,20 @@ class GroupListItem extends React.Component<Props> {
const membershipsInGroup = groupMemberships.inGroup(group.id);
const users = membershipsInGroup
.slice(0, MAX_AVATAR_DISPLAY)
.map(gm => gm.user);
.map((gm) => gm.user);
const overflow = memberCount - users.length;
return (
<React.Fragment>
<>
<ListItem
title={
<Title onClick={this.handleMembersModalOpen}>{group.name}</Title>
}
subtitle={
<React.Fragment>
<>
{memberCount} member{memberCount === 1 ? "" : "s"}
</React.Fragment>
</>
}
actions={
<Flex align="center">
@@ -79,7 +79,7 @@ class GroupListItem extends React.Component<Props> {
>
<GroupMembers group={group} onSubmit={this.handleMembersModalClose} />
</Modal>
</React.Fragment>
</>
);
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ import styled from "styled-components";
const HelpText = styled.p`
margin-top: 0;
color: ${props => props.theme.textSecondary};
font-size: ${props => (props.small ? "13px" : "inherit")};
color: ${(props) => props.theme.textSecondary};
font-size: ${(props) => (props.small ? "13px" : "inherit")};
`;
export default HelpText;
+1 -1
View File
@@ -38,7 +38,7 @@ function Highlight({
}
const Mark = styled.mark`
background: ${props => props.theme.yellow};
background: ${(props) => props.theme.yellow};
border-radius: 2px;
padding: 0 4px;
`;
+47 -46
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { inject } from "mobx-react";
import { transparentize } from "polished";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import styled from "styled-components";
import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import isInternalUrl from "utils/isInternalUrl";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import isInternalUrl from "utils/isInternalUrl";
const DELAY_OPEN = 300;
const DELAY_CLOSE = 300;
@@ -31,7 +31,7 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
const [isVisible, setVisible] = React.useState(false);
const timerClose = React.useRef();
const timerOpen = React.useRef();
const cardRef = React.useRef();
const cardRef = React.useRef<?HTMLDivElement>();
const startCloseTimer = () => {
stopOpenTimer();
@@ -57,42 +57,43 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
}
};
React.useEffect(
() => {
if (slug) {
documents.prefetchDocument(slug, {
prefetch: true,
});
}
React.useEffect(() => {
if (slug) {
documents.prefetchDocument(slug, {
prefetch: true,
});
}
startOpenTimer();
startOpenTimer();
if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.addEventListener("mouseleave", startCloseTimer);
}
node.addEventListener("mouseout", startCloseTimer);
node.addEventListener("mouseover", stopCloseTimer);
node.addEventListener("mouseover", startOpenTimer);
return () => {
node.removeEventListener("mouseout", startCloseTimer);
node.removeEventListener("mouseover", stopCloseTimer);
node.removeEventListener("mouseover", startOpenTimer);
if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
cardRef.current.addEventListener("mouseleave", startCloseTimer);
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
node.addEventListener("mouseout", startCloseTimer);
node.addEventListener("mouseover", stopCloseTimer);
node.addEventListener("mouseover", startOpenTimer);
return () => {
node.removeEventListener("mouseout", startCloseTimer);
node.removeEventListener("mouseover", stopCloseTimer);
node.removeEventListener("mouseover", startOpenTimer);
if (cardRef.current) {
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
},
[node]
);
if (timerClose.current) {
clearTimeout(timerClose.current);
}
};
}, [node]);
const anchorBounds = node.getBoundingClientRect();
const cardBounds = cardRef.current
@@ -112,7 +113,7 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
>
<div ref={cardRef}>
<HoverPreviewDocument url={node.href}>
{content =>
{(content) =>
isVisible ? (
<Animate>
<Card>
@@ -156,8 +157,8 @@ const CardContent = styled.div`
// &:after — gradient mask for overflow text
const Card = styled.div`
backdrop-filter: blur(10px);
background: ${props => props.theme.background};
border: ${props =>
background: ${(props) => props.theme.background};
border: ${(props) =>
props.theme.menuBorder ? `1px solid ${props.theme.menuBorder}` : "none"};
border-radius: 4px;
box-shadow: 0 30px 90px -20px rgba(0, 0, 0, 0.3),
@@ -179,15 +180,15 @@ const Card = styled.div`
pointer-events: none;
background: linear-gradient(
90deg,
${props => transparentize(1, props.theme.background)} 0%,
${props => transparentize(1, props.theme.background)} 75%,
${props => props.theme.background} 90%
${(props) => transparentize(1, props.theme.background)} 0%,
${(props) => transparentize(1, props.theme.background)} 75%,
${(props) => props.theme.background} 90%
);
bottom: 0;
left: 0;
right: 0;
height: 1.7em;
border-bottom: 16px solid ${props => props.theme.background};
border-bottom: 16px solid ${(props) => props.theme.background};
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
@@ -205,7 +206,7 @@ const Position = styled.div`
const Pointer = styled.div`
top: -22px;
left: ${props => props.offset}px;
left: ${(props) => props.offset}px;
width: 22px;
height: 22px;
position: absolute;
@@ -222,14 +223,14 @@ const Pointer = styled.div`
&:before {
border: 8px solid transparent;
border-bottom-color: ${props =>
border-bottom-color: ${(props) =>
props.theme.menuBorder || "rgba(0, 0, 0, 0.1)"};
right: -1px;
}
&:after {
border: 7px solid transparent;
border-bottom-color: ${props => props.theme.background};
border-bottom-color: ${(props) => props.theme.background};
}
`;
+6 -6
View File
@@ -1,17 +1,17 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import Editor from "components/Editor";
import styled from "styled-components";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMeta from "components/DocumentMeta";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
type Props = {
url: string,
documents: DocumentsStore,
children: React.Node => React.Node,
children: (React.Node) => React.Node,
};
function HoverPreviewDocument({ url, documents, children }: Props) {
@@ -27,7 +27,7 @@ function HoverPreviewDocument({ url, documents, children }: Props) {
return children(
<Content to={document.url}>
<Heading>{document.titleWithDefault}</Heading>
<DocumentMeta isDraft={document.isDraft} document={document} />
<DocumentMetaWithViews isDraft={document.isDraft} document={document} />
<Editor
key={document.id}
@@ -45,7 +45,7 @@ const Content = styled(Link)`
const Heading = styled.h2`
margin: 0 0 0.75em;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
`;
export default inject("documents")(observer(HoverPreviewDocument));
+8 -8
View File
@@ -1,8 +1,6 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { TwitterPicker } from "react-color";
import {
CollectionIcon,
CoinsIcon,
@@ -23,11 +21,13 @@ import {
SunIcon,
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import { TwitterPicker } from "react-color";
import styled from "styled-components";
import { LabelText } from "components/Input";
import { DropdownMenu } from "components/DropdownMenu";
import NudeButton from "components/NudeButton";
import Flex from "components/Flex";
import { LabelText } from "components/Input";
import NudeButton from "components/NudeButton";
export const icons = {
collection: {
@@ -166,7 +166,7 @@ class IconPicker extends React.Component<Props> {
const Component = icons[this.props.icon || "collection"].component;
return (
<Wrapper ref={ref => (this.node = ref)}>
<Wrapper ref={(ref) => (this.node = ref)}>
<label>
<LabelText>Icon</LabelText>
</label>
@@ -179,7 +179,7 @@ class IconPicker extends React.Component<Props> {
}
>
<Icons onClick={preventEventBubble}>
{Object.keys(icons).map(name => {
{Object.keys(icons).map((name) => {
const Component = icons[name].component;
return (
<IconButton
@@ -195,7 +195,7 @@ class IconPicker extends React.Component<Props> {
<Flex onClick={preventEventBubble}>
<ColorPicker
color={this.props.color}
onChange={color =>
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
@@ -214,7 +214,7 @@ const Icons = styled.div`
`;
const LabelButton = styled(NudeButton)`
border: 1px solid ${props => props.theme.inputBorder};
border: 1px solid ${(props) => props.theme.inputBorder};
width: 32px;
height: 32px;
`;
+17 -16
View File
@@ -1,37 +1,37 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import VisuallyHidden from "components/VisuallyHidden";
import Flex from "components/Flex";
import VisuallyHidden from "components/VisuallyHidden";
const RealTextarea = styled.textarea`
border: 0;
flex: 1;
padding: 8px 12px 8px ${props => (props.hasIcon ? "8px" : "12px")};
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
`;
const RealInput = styled.input`
border: 0;
flex: 1;
padding: 8px 12px 8px ${props => (props.hasIcon ? "8px" : "12px")};
padding: 8px 12px 8px ${(props) => (props.hasIcon ? "8px" : "12px")};
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
height: 30px;
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
&::-webkit-search-cancel-button {
@@ -40,8 +40,8 @@ const RealInput = styled.input`
`;
const Wrapper = styled.div`
flex: ${props => (props.flex ? "1" : "0")};
max-width: ${props => (props.short ? "350px" : "100%")};
flex: ${(props) => (props.flex ? "1" : "0")};
max-width: ${(props) => (props.short ? "350px" : "100%")};
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "initial")};
`;
@@ -56,16 +56,17 @@ const IconWrapper = styled.span`
export const Outline = styled(Flex)`
display: flex;
flex: 1;
margin: ${props => (props.margin !== undefined ? props.margin : "0 0 16px")};
margin: ${(props) =>
props.margin !== undefined ? props.margin : "0 0 16px"};
color: inherit;
border-width: 1px;
border-style: solid;
border-color: ${props =>
border-color: ${(props) =>
props.hasError
? "red"
: props.focused
? props.theme.inputBorderFocused
: props.theme.inputBorder};
? props.theme.inputBorderFocused
: props.theme.inputBorder};
border-radius: 4px;
font-weight: normal;
align-items: center;
@@ -147,7 +148,7 @@ class Input extends React.Component<Props> {
<Outline focused={this.focused} margin={margin}>
{icon && <IconWrapper>{icon}</IconWrapper>}
<InputComponent
ref={ref => (this.input = ref)}
ref={(ref) => (this.input = ref)}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
type={type === "textarea" ? undefined : type}
+18 -12
View File
@@ -1,8 +1,9 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
import { LabelText, Outline } from "components/Input";
type Props = {
@@ -10,6 +11,7 @@ type Props = {
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
};
@observer
@@ -34,21 +36,23 @@ class InputRich extends React.Component<Props> {
const EditorImport = await import("./Editor");
this.editorComponent = EditorImport.default;
} catch (err) {
console.error(err);
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
window.location.reload();
if (err.message && err.message.match(/chunk/)) {
// If the editor bundle fails to load then reload the entire window. This
// can happen if a deploy happens between the user loading the initial JS
// bundle and the async-loaded editor JS bundle as the hash will change.
window.location.reload();
return;
}
throw err;
}
};
render() {
const { label, minHeight, maxHeight, ...rest } = this.props;
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
const Editor = this.editorComponent;
return (
<React.Fragment>
<>
<LabelText>{label}</LabelText>
<StyledOutline
@@ -60,6 +64,7 @@ class InputRich extends React.Component<Props> {
<Editor
onBlur={this.handleBlur}
onFocus={this.handleFocus}
ui={ui}
grow
{...rest}
/>
@@ -67,12 +72,13 @@ class InputRich extends React.Component<Props> {
"Loading…"
)}
</StyledOutline>
</React.Fragment>
</>
);
}
}
const StyledOutline = styled(Outline)`
display: block;
padding: 8px 12px;
min-height: ${({ minHeight }) => (minHeight ? `${minHeight}px` : "0")};
max-height: ${({ maxHeight }) => (maxHeight ? `${maxHeight}px` : "auto")};
@@ -83,4 +89,4 @@ const StyledOutline = styled(Outline)`
}
`;
export default withTheme(InputRich);
export default inject("ui")(withTheme(InputRich));
+6 -6
View File
@@ -1,13 +1,13 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { SearchIcon } from "outline-icons";
import * as React from "react";
import keydown from "react-keydown";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { withRouter, type RouterHistory } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import { SearchIcon } from "outline-icons";
import { searchUrl } from "utils/routeHelpers";
import Input from "./Input";
import { searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
@@ -30,7 +30,7 @@ class InputSearch extends React.Component<Props> {
}
}
handleSearchInput = ev => {
handleSearchInput = (ev) => {
ev.preventDefault();
this.props.history.push(
searchUrl(ev.target.value, this.props.collectionId)
@@ -50,7 +50,7 @@ class InputSearch extends React.Component<Props> {
return (
<InputMaxWidth
ref={ref => (this.input = ref)}
ref={(ref) => (this.input = ref)}
type="search"
placeholder={placeholder}
onInput={this.handleSearchInput}
+5 -5
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import VisuallyHidden from "components/VisuallyHidden";
import { Outline, LabelText } from "./Input";
@@ -12,11 +12,11 @@ const Select = styled.select`
padding: 8px 12px;
outline: none;
background: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
&:disabled,
&::placeholder {
color: ${props => props.theme.placeholder};
color: ${(props) => props.theme.placeholder};
}
`;
@@ -57,7 +57,7 @@ class InputSelect extends React.Component<Props> {
))}
<Outline focused={this.focused} className={className}>
<Select onBlur={this.handleBlur} onFocus={this.handleFocus} {...rest}>
{options.map(option => (
{options.map((option) => (
<option value={option.value} key={option.value}>
{option.label}
</option>
+5 -5
View File
@@ -7,13 +7,13 @@ const Key = styled.kbd`
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 10px;
color: ${props => props.theme.almostBlack};
color: ${(props) => props.theme.almostBlack};
vertical-align: middle;
background-color: ${props => props.theme.smokeLight};
border: solid 1px ${props => props.theme.slateLight};
border-bottom-color: ${props => props.theme.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 ${props => props.theme.slate};
box-shadow: inset 0 -1px 0 ${(props) => props.theme.slate};
`;
export default Key;
+3 -3
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import Flex from "components/Flex";
import * as React from "react";
import styled from "styled-components";
import Flex from "components/Flex";
type Props = {
label: React.Node | string,
@@ -21,7 +21,7 @@ export const Label = styled(Flex)`
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
letter-spacing: 0.04em;
`;
+20 -22
View File
@@ -1,33 +1,32 @@
// @flow
import * as React from "react";
import { Switch, Route, Redirect } from "react-router-dom";
import { Helmet } from "react-helmet";
import styled, { withTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
import ErrorSuspended from "scenes/ErrorSuspended";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import Analytics from "components/Analytics";
import DocumentHistory from "components/DocumentHistory";
import { GlobalStyles } from "components/DropToImport";
import Flex from "components/Flex";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import {
homeUrl,
searchUrl,
matchDocumentSlug as slug,
} from "utils/routeHelpers";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import { GlobalStyles } from "components/DropToImport";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import Modals from "components/Modals";
import DocumentHistory from "components/DocumentHistory";
import Modal from "components/Modal";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import ErrorSuspended from "scenes/ErrorSuspended";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
type Props = {
documents: DocumentsStore,
children?: ?React.Node,
@@ -127,7 +126,6 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modals ui={ui} />
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -142,8 +140,8 @@ class Layout extends React.Component<Props> {
}
const Container = styled(Flex)`
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
position: relative;
width: 100%;
min-height: 100%;
@@ -158,7 +156,7 @@ const Content = styled(Flex)`
}
${breakpoint("tablet")`
margin-left: ${props => (props.editMode ? 0 : props.theme.sidebarWidth)};
margin-left: ${(props) => (props.editMode ? 0 : props.theme.sidebarWidth)};
`};
`;
+3 -3
View File
@@ -27,9 +27,9 @@ const ListItem = ({ image, title, subtitle, actions }: Props) => {
const Wrapper = styled.li`
display: flex;
padding: ${props => (props.compact ? "8px" : "12px")} 0;
padding: ${(props) => (props.compact ? "8px" : "12px")} 0;
margin: 0;
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
&:last-child {
border-bottom: 0;
@@ -59,7 +59,7 @@ const Content = styled(Flex)`
const Subtitle = styled.p`
margin: 0;
font-size: 14px;
color: ${props => props.theme.slate};
color: ${(props) => props.theme.slate};
`;
const Actions = styled.div`
+3 -3
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Mask from "components/Mask";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
type Props = {
count?: number,
@@ -13,7 +13,7 @@ type Props = {
const Placeholder = ({ count }: Props) => {
return (
<Fade>
{times(count || 2, index => (
{times(count || 2, (index) => (
<Item key={index} column auto>
<Mask />
</Item>
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import UiStore from "stores/UiStore";
type Props = {
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { times } from "lodash";
import * as React from "react";
import styled from "styled-components";
import Mask from "components/Mask";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
type Props = {
count?: number,
@@ -13,7 +13,7 @@ type Props = {
const ListPlaceHolder = ({ count }: Props) => {
return (
<Fade>
{times(count || 2, index => (
{times(count || 2, (index) => (
<Item key={index} column auto>
<Mask header />
<Mask />
@@ -1,21 +1,24 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Mask from "components/Mask";
import DelayedMount from "components/DelayedMount";
import Fade from "components/Fade";
import Flex from "components/Flex";
import Mask from "components/Mask";
export default function LoadingPlaceholder(props: Object) {
return (
<Wrapper>
<Flex column auto {...props}>
<Mask height={34} />
<br />
<Mask />
<Mask />
<Mask />
</Flex>
</Wrapper>
<DelayedMount>
<Wrapper>
<Flex column auto {...props}>
<Mask height={34} />
<br />
<Mask />
<Mask />
<Mask />
</Flex>
</Wrapper>
</DelayedMount>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import LoadingPlaceholder from "./LoadingPlaceholder";
import ListPlaceholder from "./ListPlaceholder";
import LoadingPlaceholder from "./LoadingPlaceholder";
export default LoadingPlaceholder;
export { ListPlaceholder };
+5 -4
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import { pulsate } from "shared/styles/animations";
import { randomInteger } from "shared/random";
import { pulsate } from "shared/styles/animations";
import Flex from "components/Flex";
type Props = {
@@ -27,10 +27,11 @@ class Mask extends React.Component<Props> {
}
const Redacted = styled(Flex)`
width: ${props => (props.header ? props.width / 2 : props.width)}%;
height: ${props => (props.height ? props.height : props.header ? 24 : 18)}px;
width: ${(props) => (props.header ? props.width / 2 : props.width)}%;
height: ${(props) =>
props.height ? props.height : props.header ? 24 : 18}px;
margin-bottom: 6px;
background-color: ${props => props.theme.divider};
background-color: ${(props) => props.theme.divider};
animation: ${pulsate} 1.3s infinite;
&:last-child {
+14 -14
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { CloseIcon, BackIcon } from "outline-icons";
import { transparentize } from "polished";
import * as React from "react";
import ReactModal from "react-modal";
import styled, { createGlobalStyle } from "styled-components";
import breakpoint from "styled-components-breakpoint";
import ReactModal from "react-modal";
import { transparentize } from "polished";
import { CloseIcon, BackIcon } from "outline-icons";
import NudeButton from "components/NudeButton";
import { fadeAndScaleIn } from "shared/styles/animations";
import Flex from "components/Flex";
import NudeButton from "components/NudeButton";
ReactModal.setAppElement("#root");
@@ -21,7 +21,7 @@ type Props = {
const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {
background-color: ${props =>
background-color: ${(props) =>
transparentize(0.25, props.theme.background)} !important;
z-index: 100;
}
@@ -30,7 +30,7 @@ const GlobalStyles = createGlobalStyle`
.ReactModalPortal + .ReactModalPortal {
.ReactModal__Overlay {
margin-left: 12px;
box-shadow: 0 -2px 10px ${props => props.theme.shadow};
box-shadow: 0 -2px 10px ${(props) => props.theme.shadow};
border-radius: 8px 0 0 8px;
overflow: hidden;
}
@@ -64,7 +64,7 @@ const Modal = ({
if (!isOpen) return null;
return (
<React.Fragment>
<>
<GlobalStyles />
<StyledModal
contentLabel={title}
@@ -72,7 +72,7 @@ const Modal = ({
isOpen={isOpen}
{...rest}
>
<Content onClick={ev => ev.stopPropagation()} column>
<Content onClick={(ev) => ev.stopPropagation()} column>
{title && <h1>{title}</h1>}
{children}
@@ -85,7 +85,7 @@ const Modal = ({
<CloseIcon size={32} color="currentColor" />
</Close>
</StyledModal>
</React.Fragment>
</>
);
};
@@ -109,8 +109,8 @@ const StyledModal = styled(ReactModal)`
align-items: flex-start;
overflow-x: hidden;
overflow-y: auto;
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};
background: ${(props) => props.theme.background};
transition: ${(props) => props.theme.backgroundTransition};
padding: 8vh 2rem 2rem;
outline: none;
@@ -133,7 +133,7 @@ const Close = styled(NudeButton)`
right: 0;
margin: 12px;
opacity: 0.75;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
width: auto;
height: auto;
@@ -153,7 +153,7 @@ const Back = styled(NudeButton)`
top: 2rem;
left: 2rem;
opacity: 0.75;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
width: auto;
height: auto;
-58
View File
@@ -1,58 +0,0 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import BaseModal from "components/Modal";
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 DocumentShare from "scenes/DocumentShare";
type Props = {
ui: UiStore,
};
@observer
class Modals extends React.Component<Props> {
handleClose = () => {
this.props.ui.clearActiveModal();
};
render() {
const { activeModalName, activeModalProps } = this.props.ui;
const Modal = ({ name, children, ...rest }) => {
return (
<BaseModal
isOpen={activeModalName === name}
onRequestClose={this.handleClose}
{...rest}
>
{React.cloneElement(children, activeModalProps)}
</BaseModal>
);
};
return (
<span>
<Modal name="collection-new" title="Create a collection">
<CollectionNew onSubmit={this.handleClose} />
</Modal>
<Modal name="collection-edit" title="Edit collection">
<CollectionEdit onSubmit={this.handleClose} />
</Modal>
<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>
</span>
);
}
}
export default Modals;
+2 -2
View File
@@ -2,8 +2,8 @@
import styled from "styled-components";
const Notice = styled.p`
background: ${props => props.theme.sidebarBackground};
color: ${props => props.theme.sidebarText};
background: ${(props) => props.theme.sidebarBackground};
color: ${(props) => props.theme.sidebarText};
padding: 10px 12px;
border-radius: 4px;
position: relative;
+3 -3
View File
@@ -1,7 +1,7 @@
// @flow
import { lighten } from "polished";
import * as React from "react";
import styled from "styled-components";
import { lighten } from "polished";
const Button = styled.button`
width: 24px;
@@ -14,12 +14,12 @@ const Button = styled.button`
&:focus {
transition-duration: 0.05s;
box-shadow: ${props => lighten(0.4, props.theme.buttonBackground)} 0px 0px
box-shadow: ${(props) => lighten(0.4, props.theme.buttonBackground)} 0px 0px
0px 3px;
outline: none;
}
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<any, typeof Button>((props, ref) => (
<Button {...props} ref={ref} />
));
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import { Helmet } from "react-helmet";
import AuthStore from "stores/AuthStore";
+2 -2
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import * as React from "react";
import Document from "models/Document";
import DocumentPreview from "components/DocumentPreview";
import PaginatedList from "components/PaginatedList";
@@ -25,7 +25,7 @@ class PaginatedDocumentList extends React.Component<Props> {
heading={heading}
fetch={fetch}
options={options}
renderItem={item => (
renderItem={(item) => (
<DocumentPreview key={item.id} document={item} {...rest} />
)}
/>
+13 -9
View File
@@ -1,11 +1,11 @@
// @flow
import * as React from "react";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Waypoint } from "react-waypoint";
import ArrowKeyNavigation from "boundless-arrow-key-navigation";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DelayedMount from "components/DelayedMount";
import { ListPlaceholder } from "components/LoadingPlaceholder";
type Props = {
@@ -14,7 +14,7 @@ type Props = {
heading?: React.Node,
empty?: React.Node,
items: any[],
renderItem: any => React.Node,
renderItem: (any) => React.Node,
};
@observer
@@ -96,10 +96,10 @@ class PaginatedList extends React.Component<Props> {
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
return (
<React.Fragment>
<>
{showEmpty && empty}
{showList && (
<React.Fragment>
<>
{heading}
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
@@ -110,10 +110,14 @@ class PaginatedList extends React.Component<Props> {
{this.allowLoadMore && (
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
)}
</React.Fragment>
</>
)}
{showLoading && <ListPlaceholder count={5} />}
</React.Fragment>
{showLoading && (
<DelayedMount>
<ListPlaceholder count={5} />
</DelayedMount>
)}
</>
);
}
}
+8 -9
View File
@@ -1,14 +1,13 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import styled from "styled-components";
import { GoToIcon } from "outline-icons";
import Flex from "components/Flex";
import Document from "models/Document";
import Collection from "models/Collection";
import * as React from "react";
import styled from "styled-components";
import type { DocumentPath } from "stores/CollectionsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import CollectionIcon from "components/CollectionIcon";
import Flex from "components/Flex";
type Props = {
result: DocumentPath,
@@ -44,7 +43,7 @@ class PathToDocument extends React.Component<Props> {
<Component ref={ref} onClick={this.handleClick} href="" selectable>
{collection && <CollectionIcon collection={collection} />}
{result.path
.map(doc => <Title key={doc.id}>{doc.title}</Title>)
.map((doc) => <Title key={doc.id}>{doc.title}</Title>)
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
{document && (
<Flex>
@@ -73,7 +72,7 @@ const ResultWrapper = styled.div`
margin-left: -4px;
user-select: none;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
cursor: default;
`;
@@ -85,7 +84,7 @@ const ResultWrapperLink = styled(ResultWrapper.withComponent("a"))`
&:hover,
&:active,
&:focus {
background: ${props => props.theme.listItemHoverBackground};
background: ${(props) => props.theme.listItemHoverBackground};
outline: none;
}
`;
+1 -1
View File
@@ -1,6 +1,6 @@
// @flow
import * as React from "react";
import BoundlessPopover from "boundless-popover";
import * as React from "react";
import styled, { keyframes } from "styled-components";
const fadeIn = keyframes`
-121
View File
@@ -1,121 +0,0 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import styled from "styled-components";
import Document from "models/Document";
import Flex from "components/Flex";
import Time from "components/Time";
import Breadcrumb from "components/Breadcrumb";
import CollectionsStore from "stores/CollectionsStore";
import AuthStore from "stores/AuthStore";
const Container = styled(Flex)`
color: ${props => props.theme.textTertiary};
font-size: 13px;
white-space: nowrap;
overflow: hidden;
`;
const Modified = styled.span`
color: ${props =>
props.highlight ? props.theme.text : props.theme.textTertiary};
font-weight: ${props => (props.highlight ? "600" : "400")};
`;
type Props = {
collections: CollectionsStore,
auth: AuthStore,
showCollection?: boolean,
showPublished?: boolean,
document: Document,
children: React.Node,
};
function PublishingInfo({
auth,
collections,
showPublished,
showCollection,
document,
children,
...rest
}: Props) {
const {
modifiedSinceViewed,
updatedAt,
updatedBy,
createdAt,
publishedAt,
archivedAt,
deletedAt,
isDraft,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
// Currently the situation where this is true is rendering share links.
if (!updatedBy) {
return null;
}
let content;
if (deletedAt) {
content = (
<span>
deleted <Time dateTime={deletedAt} /> ago
</span>
);
} else if (archivedAt) {
content = (
<span>
archived <Time dateTime={archivedAt} /> ago
</span>
);
} else if (createdAt === updatedAt) {
content = (
<span>
created <Time dateTime={updatedAt} /> ago
</span>
);
} else if (publishedAt && (publishedAt === updatedAt || showPublished)) {
content = (
<span>
published <Time dateTime={publishedAt} /> ago
</span>
);
} else if (isDraft) {
content = (
<span>
saved <Time dateTime={updatedAt} /> ago
</span>
);
} else {
content = (
<Modified highlight={modifiedSinceViewed}>
updated <Time dateTime={updatedAt} /> ago
</Modified>
);
}
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
return (
<Container align="center" {...rest}>
{updatedByMe ? "You" : updatedBy.name}&nbsp;
{content}
{showCollection &&
collection && (
<span>
&nbsp;in&nbsp;
<strong>
<Breadcrumb document={document} onlyText />
</strong>
</span>
)}
{children}
</Container>
);
}
export default inject("collections", "auth")(observer(PublishingInfo));
+2 -2
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
type Props = {
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
overflow-x: hidden;
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
box-shadow: ${props =>
box-shadow: ${(props) =>
props.shadow ? "0 1px inset rgba(0,0,0,.1)" : "none"};
transition: all 250ms ease-in-out;
`;
+36 -23
View File
@@ -1,7 +1,6 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import styled from "styled-components";
import {
ArchiveIcon,
HomeIcon,
@@ -12,44 +11,48 @@ import {
TrashIcon,
PlusIcon,
} from "outline-icons";
import Flex from "components/Flex";
import Modal from "components/Modal";
import Invite from "scenes/Invite";
import AccountMenu from "menus/AccountMenu";
import Sidebar from "./Sidebar";
import Scrollable from "components/Scrollable";
import Section from "./components/Section";
import Collections from "./components/Collections";
import SidebarLink from "./components/SidebarLink";
import HeaderBlock from "./components/HeaderBlock";
import Bubble from "./components/Bubble";
import * as React from "react";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import { observable } from "mobx";
import CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
import Flex from "components/Flex";
import Modal from "components/Modal";
import Scrollable from "components/Scrollable";
import Sidebar from "./Sidebar";
import Bubble from "./components/Bubble";
import Collections from "./components/Collections";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import AccountMenu from "menus/AccountMenu";
type Props = {
auth: AuthStore,
documents: DocumentsStore,
policies: PoliciesStore,
ui: UiStore,
};
@observer
class MainSidebar extends React.Component<Props> {
@observable inviteModalOpen: boolean = false;
@observable inviteModalOpen = false;
@observable createCollectionModalOpen = false;
componentDidMount() {
this.props.documents.fetchDrafts();
this.props.documents.fetchTemplates();
}
handleCreateCollection = (ev: SyntheticEvent<>) => {
handleCreateCollectionModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.props.ui.setActiveModal("collection-new");
this.createCollectionModalOpen = true;
};
handleCreateCollectionModalClose = (ev: SyntheticEvent<>) => {
this.createCollectionModalOpen = false;
};
handleInviteModalOpen = (ev: SyntheticEvent<>) => {
@@ -119,7 +122,8 @@ class MainSidebar extends React.Component<Props> {
icon={<EditIcon color="currentColor" />}
label={
<Drafts align="center">
Drafts{draftDocumentsCount > 0 && (
Drafts
{draftDocumentsCount > 0 && (
<Bubble count={draftDocumentsCount} />
)}
</Drafts>
@@ -134,7 +138,9 @@ class MainSidebar extends React.Component<Props> {
/>
</Section>
<Section>
<Collections onCreateCollection={this.handleCreateCollection} />
<Collections
onCreateCollection={this.handleCreateCollectionModalOpen}
/>
</Section>
<Section>
<SidebarLink
@@ -175,6 +181,13 @@ class MainSidebar extends React.Component<Props> {
>
<Invite onSubmit={this.handleInviteModalClose} />
</Modal>
<Modal
title="Create a collection"
onRequestClose={this.handleCreateCollectionModalClose}
isOpen={this.createCollectionModalOpen}
>
<CollectionNew onSubmit={this.handleCreateCollectionModalClose} />
</Modal>
</Sidebar>
);
}
@@ -184,4 +197,4 @@ const Drafts = styled(Flex)`
height: 24px;
`;
export default inject("documents", "policies", "auth", "ui")(MainSidebar);
export default inject("documents", "policies", "auth")(MainSidebar);
+21 -22
View File
@@ -1,8 +1,5 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import type { RouterHistory } from "react-router-dom";
import styled from "styled-components";
import {
DocumentIcon,
EmailIcon,
@@ -16,19 +13,22 @@ import {
BulletedListIcon,
ExpandedIcon,
} from "outline-icons";
import ZapierIcon from "./icons/Zapier";
import SlackIcon from "./icons/Slack";
import Flex from "components/Flex";
import Sidebar from "./Sidebar";
import Scrollable from "components/Scrollable";
import Section from "./components/Section";
import Header from "./components/Header";
import SidebarLink from "./components/SidebarLink";
import HeaderBlock from "./components/HeaderBlock";
import Version from "./components/Version";
import PoliciesStore from "stores/PoliciesStore";
import * as React from "react";
import type { RouterHistory } from "react-router-dom";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import PoliciesStore from "stores/PoliciesStore";
import Flex from "components/Flex";
import Scrollable from "components/Scrollable";
import Sidebar from "./Sidebar";
import Header from "./components/Header";
import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
import Version from "./components/Version";
import SlackIcon from "./icons/Slack";
import ZapierIcon from "./icons/Zapier";
import env from "env";
type Props = {
@@ -146,13 +146,12 @@ class SettingsSidebar extends React.Component<Props> {
/>
</Section>
)}
{can.update &&
env.DEPLOYMENT !== "hosted" && (
<Section>
<Header>Installation</Header>
<Version />
</Section>
)}
{can.update && env.DEPLOYMENT !== "hosted" && (
<Section>
<Header>Installation</Header>
<Version />
</Section>
)}
</Scrollable>
</Flex>
</Sidebar>
+12 -11
View File
@@ -1,14 +1,14 @@
// @flow
import { observer, inject } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
import * as React from "react";
import { withRouter } from "react-router-dom";
import type { Location } from "react-router-dom";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { observer, inject } from "mobx-react";
import { CloseIcon, MenuIcon } from "outline-icons";
import UiStore from "stores/UiStore";
import Fade from "components/Fade";
import Flex from "components/Flex";
import UiStore from "stores/UiStore";
let firstRender = true;
@@ -67,9 +67,10 @@ const Container = styled(Flex)`
top: 0;
bottom: 0;
width: 100%;
background: ${props => props.theme.sidebarBackground};
transition: left 100ms ease-out, ${props => props.theme.backgroundTransition};
margin-left: ${props => (props.mobileSidebarVisible ? 0 : "-100%")};
background: ${(props) => props.theme.sidebarBackground};
transition: left 100ms ease-out,
${(props) => props.theme.backgroundTransition};
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
z-index: 1000;
@media print {
@@ -80,7 +81,7 @@ const Container = styled(Flex)`
&:before,
&:after {
content: "";
background: ${props => props.theme.sidebarBackground};
background: ${(props) => props.theme.sidebarBackground};
position: absolute;
top: -50vh;
left: 0;
@@ -94,8 +95,8 @@ const Container = styled(Flex)`
}
${breakpoint("tablet")`
left: ${props => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
width: ${props => props.theme.sidebarWidth};
left: ${(props) => (props.editMode ? `-${props.theme.sidebarWidth}` : 0)};
width: ${(props) => props.theme.sidebarWidth};
margin: 0;
z-index: 3;
`};
@@ -106,8 +107,8 @@ const Toggle = styled.a`
align-items: center;
position: fixed;
top: 0;
left: ${props => (props.mobileSidebarVisible ? "auto" : 0)};
right: ${props => (props.mobileSidebarVisible ? 0 : "auto")};
left: ${(props) => (props.mobileSidebarVisible ? "auto" : 0)};
right: ${(props) => (props.mobileSidebarVisible ? 0 : "auto")};
z-index: 1;
margin: 12px;
+2 -2
View File
@@ -14,8 +14,8 @@ const Bubble = ({ count }: Props) => {
const Count = styled.div`
animation: ${bounceIn} 600ms;
transform-origin: center center;
color: ${props => props.theme.white};
background: ${props => props.theme.slateDark};
color: ${(props) => props.theme.white};
background: ${(props) => props.theme.slateDark};
display: inline-block;
font-feature-settings: "tnum";
font-weight: 600;
@@ -1,17 +1,17 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import DocumentsStore from "stores/DocumentsStore";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import Document from "models/Document";
import CollectionMenu from "menus/CollectionMenu";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
import SidebarLink from "./SidebarLink";
import DocumentLink from "./DocumentLink";
import CollectionIcon from "components/CollectionIcon";
import DropToImport from "components/DropToImport";
import Flex from "components/Flex";
import DocumentLink from "./DocumentLink";
import SidebarLink from "./SidebarLink";
import CollectionMenu from "menus/CollectionMenu";
type Props = {
collection: Collection,
@@ -61,7 +61,7 @@ class CollectionLink extends React.Component<Props> {
}
>
<Flex column>
{collection.documents.map(node => (
{collection.documents.map((node) => (
<DocumentLink
key={node.id}
node={node}
@@ -1,22 +1,21 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import { withRouter, type RouterHistory } from "react-router-dom";
import keydown from "react-keydown";
import Flex from "components/Flex";
import { PlusIcon } from "outline-icons";
import { newDocumentUrl } from "utils/routeHelpers";
import Header from "./Header";
import SidebarLink from "./SidebarLink";
import CollectionLink from "./CollectionLink";
import CollectionsLoading from "./CollectionsLoading";
import Fade from "components/Fade";
import * as React from "react";
import keydown from "react-keydown";
import { withRouter, type RouterHistory } from "react-router-dom";
import CollectionsStore from "stores/CollectionsStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
import Fade from "components/Fade";
import Flex from "components/Flex";
import CollectionLink from "./CollectionLink";
import CollectionsLoading from "./CollectionsLoading";
import Header from "./Header";
import SidebarLink from "./SidebarLink";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
@@ -56,8 +55,8 @@ class Collections extends React.Component<Props> {
const { collections, ui, documents } = this.props;
const content = (
<React.Fragment>
{collections.orderedData.map(collection => (
<>
{collections.orderedData.map((collection) => (
<CollectionLink
key={collection.id}
documents={documents}
@@ -74,7 +73,7 @@ class Collections extends React.Component<Props> {
label="New collection…"
exact
/>
</React.Fragment>
</>
);
return (
@@ -94,6 +93,9 @@ class Collections extends React.Component<Props> {
}
}
export default inject("collections", "ui", "documents", "policies")(
withRouter(Collections)
);
export default inject(
"collections",
"ui",
"documents",
"policies"
)(withRouter(Collections));
@@ -1,16 +1,16 @@
// @flow
import * as React from "react";
import { observer } from "mobx-react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Collection from "models/Collection";
import Document from "models/Document";
import DocumentMenu from "menus/DocumentMenu";
import SidebarLink from "./SidebarLink";
import DropToImport from "components/DropToImport";
import Fade from "components/Fade";
import Collection from "models/Collection";
import DocumentsStore from "stores/DocumentsStore";
import Flex from "components/Flex";
import SidebarLink from "./SidebarLink";
import DocumentMenu from "menus/DocumentMenu";
import { type NavigationNode } from "types";
type Props = {
@@ -76,7 +76,7 @@ class DocumentLink extends React.Component<Props> {
collection &&
(collection
.pathToDocument(activeDocument)
.map(entry => entry.id)
.map((entry) => entry.id)
.includes(node.id) ||
this.isActiveDocument())
);
@@ -110,14 +110,12 @@ class DocumentLink extends React.Component<Props> {
onClose={() => (this.menuOpen = false)}
/>
</Fade>
) : (
undefined
)
) : undefined
}
>
{this.hasChildDocuments() && (
<DocumentChildren column>
{node.children.map(childNode => (
{node.children.map((childNode) => (
<DocumentLink
key={childNode.id}
collection={collection}
+2 -2
View File
@@ -1,12 +1,12 @@
// @flow
import Flex from "components/Flex";
import styled from "styled-components";
import Flex from "components/Flex";
const Header = styled(Flex)`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 4px 16px;
`;
@@ -1,7 +1,7 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import { ExpandedIcon } from "outline-icons";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
@@ -46,7 +46,7 @@ const Subheading = styled.div`
font-size: 11px;
text-transform: uppercase;
font-weight: 500;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
`;
const TeamName = styled.div`
@@ -54,7 +54,7 @@ const TeamName = styled.div`
padding-left: 10px;
padding-right: 24px;
font-weight: 600;
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
text-decoration: none;
font-size: 16px;
`;
@@ -1,9 +1,9 @@
// @flow
import * as React from "react";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import { withRouter, NavLink } from "react-router-dom";
import { CollapsedIcon } from "outline-icons";
import * as React from "react";
import { withRouter, NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
@@ -108,11 +108,11 @@ const IconWrapper = styled.span`
`;
const Action = styled.span`
display: ${props => (props.menuOpen ? "inline" : "none")};
display: ${(props) => (props.menuOpen ? "inline" : "none")};
position: absolute;
top: 4px;
right: 4px;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
svg {
opacity: 0.75;
@@ -132,17 +132,17 @@ const StyledNavLink = styled(NavLink)`
text-overflow: ellipsis;
padding: 4px 16px;
border-radius: 4px;
color: ${props => props.theme.sidebarText};
color: ${(props) => props.theme.sidebarText};
font-size: 15px;
cursor: pointer;
&:hover {
color: ${props => props.theme.text};
color: ${(props) => props.theme.text};
}
&:focus {
color: ${props => props.theme.text};
background: ${props => props.theme.black05};
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.black05};
outline: none;
}
+3 -3
View File
@@ -2,8 +2,8 @@
import * as React from "react";
import styled from "styled-components";
import Badge from "components/Badge";
import SidebarLink from "./SidebarLink";
import { version } from "../../../../package.json";
import SidebarLink from "./SidebarLink";
export default function Version() {
const [releasesBehind, setReleasesBehind] = React.useState(0);
@@ -30,7 +30,7 @@ export default function Version() {
<SidebarLink
href="https://github.com/outline/outline/releases"
label={
<React.Fragment>
<>
v{version}
<br />
<LilBadge>
@@ -40,7 +40,7 @@ export default function Version() {
releasesBehind === 1 ? "" : "s"
} behind`}
</LilBadge>
</React.Fragment>
</>
}
/>
);
+26 -20
View File
@@ -1,18 +1,18 @@
// @flow
import * as React from "react";
import { find } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { find } from "lodash";
import * as React from "react";
import io from "socket.io-client";
import DocumentsStore from "stores/DocumentsStore";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import DocumentsStore from "stores/DocumentsStore";
import GroupsStore from "stores/GroupsStore";
import MembershipsStore from "stores/MembershipsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import PoliciesStore from "stores/PoliciesStore";
import ViewsStore from "stores/ViewsStore";
import AuthStore from "stores/AuthStore";
import UiStore from "stores/UiStore";
import ViewsStore from "stores/ViewsStore";
export const SocketContext: any = React.createContext();
@@ -61,23 +61,29 @@ class SocketProvider extends React.Component<Props> {
});
});
this.socket.on("disconnect", () => {
this.socket.on("disconnect", (reason: string) => {
// when the socket is disconnected we need to clear all presence state as
// it's no longer reliable.
presence.clear();
if (reason === "io server disconnect") {
// the disconnection was initiated by the server, need to reconnect
// manually, else the socket will automatically try to reconnect
this.socket.connect();
}
});
this.socket.on("authenticated", () => {
this.socket.authenticated = true;
});
this.socket.on("unauthorized", err => {
this.socket.on("unauthorized", (err) => {
this.socket.authenticated = false;
ui.showToast(err.message);
throw err;
});
this.socket.on("entities", async event => {
this.socket.on("entities", async (event) => {
if (event.documentIds) {
for (const documentDescriptor of event.documentIds) {
const documentId = documentDescriptor.id;
@@ -182,23 +188,23 @@ class SocketProvider extends React.Component<Props> {
}
});
this.socket.on("documents.star", event => {
this.socket.on("documents.star", (event) => {
documents.starredIds.set(event.documentId, true);
});
this.socket.on("documents.unstar", event => {
this.socket.on("documents.unstar", (event) => {
documents.starredIds.set(event.documentId, false);
});
// received when a user is given access to a collection
// if the user is us then we go ahead and load the collection from API.
this.socket.on("collections.add_user", event => {
this.socket.on("collections.add_user", (event) => {
if (auth.user && event.userId === auth.user.id) {
collections.fetch(event.collectionId, { force: true });
}
// Document policies might need updating as the permission changes
documents.inCollection(event.collectionId).forEach(document => {
documents.inCollection(event.collectionId).forEach((document) => {
policies.remove(document.id);
});
});
@@ -206,7 +212,7 @@ class SocketProvider extends React.Component<Props> {
// received when a user is removed from having access to a collection
// to keep state in sync we must update our UI if the user is us,
// or otherwise just remove any membership state we have for that user.
this.socket.on("collections.remove_user", event => {
this.socket.on("collections.remove_user", (event) => {
if (auth.user && event.userId === auth.user.id) {
collections.remove(event.collectionId);
memberships.removeCollectionMemberships(event.collectionId);
@@ -218,32 +224,32 @@ class SocketProvider extends React.Component<Props> {
// received a message from the API server that we should request
// to join a specific room. Forward that to the ws server.
this.socket.on("join", event => {
this.socket.on("join", (event) => {
this.socket.emit("join", event);
});
// received a message from the API server that we should request
// to leave a specific room. Forward that to the ws server.
this.socket.on("leave", event => {
this.socket.on("leave", (event) => {
this.socket.emit("leave", event);
});
// received whenever we join a document room, the payload includes
// userIds that are present/viewing and those that are editing.
this.socket.on("document.presence", event => {
this.socket.on("document.presence", (event) => {
presence.init(event.documentId, event.userIds, event.editingIds);
});
// received whenever a new user joins a document room, aka they
// navigate to / start viewing a document
this.socket.on("user.join", event => {
this.socket.on("user.join", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
views.touch(event.documentId, event.userId);
});
// received whenever a new user leaves a document room, aka they
// navigate away / stop viewing a document
this.socket.on("user.leave", event => {
this.socket.on("user.leave", (event) => {
presence.leave(event.documentId, event.userId);
views.touch(event.documentId, event.userId);
});
@@ -251,7 +257,7 @@ class SocketProvider extends React.Component<Props> {
// received when another client in a document room wants to change
// or update it's presence. Currently the only property is whether
// the client is in editing state or not.
this.socket.on("user.presence", event => {
this.socket.on("user.presence", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
});
}
+3 -3
View File
@@ -7,7 +7,7 @@ type Props = {
};
const H3 = styled.h3`
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
line-height: 1;
@@ -21,8 +21,8 @@ const Underline = styled("span")`
font-weight: 500;
font-size: 14px;
line-height: 1.5;
color: ${props => props.theme.textSecondary};
border-bottom: 3px solid ${props => props.theme.textSecondary};
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.textSecondary};
padding-bottom: 5px;
`;
+9 -9
View File
@@ -38,8 +38,8 @@ const Label = styled.label`
const Wrapper = styled.label`
position: relative;
display: inline-block;
width: ${props => props.width}px;
height: ${props => props.height}px;
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
margin-bottom: 4px;
margin-right: 8px;
`;
@@ -51,16 +51,16 @@ const Slider = styled.span`
left: 0;
right: 0;
bottom: 0;
background-color: ${props => props.theme.slate};
background-color: ${(props) => props.theme.slate};
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: ${props => props.height}px;
border-radius: ${(props) => props.height}px;
&:before {
position: absolute;
content: "";
height: ${props => props.height - 8}px;
width: ${props => props.height - 8}px;
height: ${(props) => props.height - 8}px;
width: ${(props) => props.height - 8}px;
left: 4px;
bottom: 4px;
background-color: white;
@@ -77,15 +77,15 @@ const HiddenInput = styled.input`
visibility: hidden;
&:checked + ${Slider} {
background-color: ${props => props.theme.primary};
background-color: ${(props) => props.theme.primary};
}
&:focus + ${Slider} {
box-shadow: 0 0 1px ${props => props.theme.primary};
box-shadow: 0 0 1px ${(props) => props.theme.primary};
}
&:checked + ${Slider}:before {
transform: translateX(${props => props.width - props.height}px);
transform: translateX(${(props) => props.width - props.height}px);
}
`;
+7 -7
View File
@@ -1,8 +1,8 @@
// @flow
import * as React from "react";
import styled, { withTheme } from "styled-components";
import { NavLink } from "react-router-dom";
import { lighten } from "polished";
import * as React from "react";
import { NavLink } from "react-router-dom";
import styled, { withTheme } from "styled-components";
type Props = {
theme: Object,
@@ -15,20 +15,20 @@ const StyledNavLink = styled(NavLink)`
display: inline-block;
font-weight: 500;
font-size: 14px;
color: ${props => props.theme.textTertiary};
color: ${(props) => props.theme.textTertiary};
margin-right: 24px;
padding-bottom: 8px;
&:hover {
color: ${props => props.theme.textSecondary};
border-bottom: 3px solid ${props => props.theme.divider};
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.divider};
padding-bottom: 5px;
}
&:focus {
outline: none;
border-bottom: 3px solid
${props => lighten(0.4, props.theme.buttonBackground)};
${(props) => lighten(0.4, props.theme.buttonBackground)};
padding-bottom: 5px;
}
`;
+2 -2
View File
@@ -3,13 +3,13 @@ import styled from "styled-components";
const Tabs = styled.nav`
position: relative;
border-bottom: 1px solid ${props => props.theme.divider};
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
`;
export const Separator = styled.span`
border-left: 1px solid ${props => props.theme.divider};
border-left: 1px solid ${(props) => props.theme.divider};
position: relative;
top: 2px;
margin-right: 24px;
+2 -2
View File
@@ -5,8 +5,8 @@ const TeamLogo = styled.img`
width: auto;
height: 38px;
border-radius: 4px;
background: ${props => props.theme.background};
border: 1px solid ${props => props.theme.divider};
background: ${(props) => props.theme.background};
border: 1px solid ${(props) => props.theme.divider};
`;
export default TeamLogo;
+4 -4
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { ThemeProvider } from "styled-components";
import { dark, light } from "shared/styles/theme";
import GlobalStyles from "shared/styles/globals";
import { dark, light } from "shared/styles/theme";
import UiStore from "stores/UiStore";
type Props = {
@@ -14,10 +14,10 @@ type Props = {
function Theme({ children, ui }: Props) {
return (
<ThemeProvider theme={ui.resolvedTheme === "dark" ? dark : light}>
<React.Fragment>
<>
<GlobalStyles />
{children}
</React.Fragment>
</>
</ThemeProvider>
);
}
+4 -4
View File
@@ -1,22 +1,22 @@
// @flow
import * as React from "react";
import Tooltip from "components/Tooltip";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import format from "date-fns/format";
import * as React from "react";
import Tooltip from "components/Tooltip";
let callbacks = [];
// This is a shared timer that fires every minute, used for
// updating all Time components across the page all at once.
setInterval(() => {
callbacks.forEach(cb => cb());
callbacks.forEach((cb) => cb());
}, 1000 * 60);
function eachMinute(fn) {
callbacks.push(fn);
return () => {
callbacks = callbacks.filter(cb => cb !== fn);
callbacks = callbacks.filter((cb) => cb !== fn);
};
}
+5 -5
View File
@@ -1,9 +1,9 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import Toast from "./components/Toast";
import UiStore from "../../stores/UiStore";
import Toast from "./components/Toast";
type Props = {
ui: UiStore,
@@ -15,7 +15,7 @@ class Toasts extends React.Component<Props> {
return (
<List>
{ui.orderedToasts.map(toast => (
{ui.orderedToasts.map((toast) => (
<Toast
key={toast.id}
toast={toast}
@@ -29,8 +29,8 @@ class Toasts extends React.Component<Props> {
const List = styled.ol`
position: fixed;
left: ${props => props.theme.hpadding};
bottom: ${props => props.theme.vpadding};
left: ${(props) => props.theme.hpadding};
bottom: ${(props) => props.theme.vpadding};
list-style: none;
margin: 0;
padding: 0;
+8 -8
View File
@@ -1,9 +1,9 @@
// @flow
import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
import { darken } from "polished";
import { fadeAndScaleIn } from "shared/styles/animations";
import type { Toast as TToast } from "../../../types";
import type { Toast as TToast } from "types";
type Props = {
onRequestClose: () => void,
@@ -61,13 +61,13 @@ const Action = styled.span`
height: 100%;
text-transform: uppercase;
font-size: 12px;
color: ${props => props.theme.toastText};
background: ${props => darken(0.05, props.theme.toastBackground)};
color: ${(props) => props.theme.toastText};
background: ${(props) => darken(0.05, props.theme.toastBackground)};
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
&:hover {
background: ${props => darken(0.1, props.theme.toastBackground)};
background: ${(props) => darken(0.1, props.theme.toastBackground)};
}
`;
@@ -76,14 +76,14 @@ const Container = styled.div`
align-items: center;
animation: ${fadeAndScaleIn} 100ms ease;
margin: 8px 0;
color: ${props => props.theme.toastText};
background: ${props => props.theme.toastBackground};
color: ${(props) => props.theme.toastText};
background: ${(props) => props.theme.toastBackground};
font-size: 15px;
border-radius: 5px;
cursor: default;
&:hover {
background: ${props => darken(0.05, props.theme.toastBackground)};
background: ${(props) => darken(0.05, props.theme.toastBackground)};
}
`;
+8 -8
View File
@@ -1,7 +1,7 @@
// @flow
import Tippy from "@tippy.js/react";
import * as React from "react";
import styled from "styled-components";
import Tippy from "@tippy.js/react";
type Props = {
tooltip: React.Node,
@@ -24,9 +24,9 @@ class Tooltip extends React.Component<Props> {
if (shortcut) {
content = (
<React.Fragment>
<>
{tooltip} &middot; <Shortcut>{shortcut}</Shortcut>
</React.Fragment>
</>
);
}
@@ -54,19 +54,19 @@ const Shortcut = styled.kbd`
font: 10px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
monospace;
line-height: 10px;
color: ${props => props.theme.tooltipBackground};
color: ${(props) => props.theme.tooltipBackground};
vertical-align: middle;
background-color: ${props => props.theme.tooltipText};
background-color: ${(props) => props.theme.tooltipText};
border-radius: 3px;
`;
const StyledTippy = styled(Tippy)`
font-size: 13px;
background-color: ${props => props.theme.tooltipBackground};
color: ${props => props.theme.tooltipText};
background-color: ${(props) => props.theme.tooltipBackground};
color: ${(props) => props.theme.tooltipText};
svg {
fill: ${props => props.theme.tooltipBackground};
fill: ${(props) => props.theme.tooltipBackground};
}
`;
+1 -3
View File
@@ -19,9 +19,7 @@ export default class Figma extends React.Component<Props> {
render() {
return (
<Frame
src={`https://www.figma.com/embed?embed_host=outline&url=${
this.props.attrs.href
}`}
src={`https://www.figma.com/embed?embed_host=outline&url=${this.props.attrs.href}`}
title="Figma Embed"
border
/>
+6 -27
View File
@@ -13,31 +13,16 @@ type Props = {|
|};
class Gist extends React.Component<Props> {
iframeNode: ?HTMLIFrameElement;
static ENABLED = [URL_REGEX];
componentDidMount() {
this.updateIframeContent();
}
get id() {
const gistUrl = new URL(this.props.attrs.href);
return gistUrl.pathname.split("/")[2];
}
updateIframeContent() {
const id = this.id;
const iframe = this.iframeNode;
updateIframeContent = (iframe: ?HTMLIFrameElement) => {
if (!iframe) return;
// We need to add some temporary content to the iframe for the document
// to be available, otherwise it's undefined on first load
const temp = document.getElementById("gist");
if (temp) {
temp.innerHTML = "";
temp.appendChild(iframe);
}
const id = this.id;
// $FlowFixMe
let doc = iframe.document;
@@ -48,28 +33,22 @@ class Gist extends React.Component<Props> {
}
const gistLink = `https://gist.github.com/${id}.js`;
const gistScript = `<script type="text/javascript" src="${
gistLink
}"></script>`;
const gistScript = `<script type="text/javascript" src="${gistLink}"></script>`;
const styles =
"<style>*{ font-size:12px; } body { margin: 0; } .gist .blob-wrapper.data { max-height:150px; overflow:auto; }</style>";
const iframeHtml = `<html><head><base target="_parent">${
styles
}</head><body>${gistScript}</body></html>`;
const iframeHtml = `<html><head><base target="_parent">${styles}</head><body>${gistScript}</body></html>`;
doc.open();
doc.writeln(iframeHtml);
doc.close();
}
};
render() {
const id = this.id;
return (
<iframe
ref={ref => {
this.iframeNode = ref;
}}
ref={this.updateIframeContent}
type="text/html"
frameBorder="0"
width="100%"
+11 -8
View File
@@ -1,19 +1,22 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
type Props = {
src?: string,
border?: boolean,
forwardedRef: *,
width?: string,
height?: string,
};
type PropsWithRef = Props & {
forwardedRef: React.Ref<typeof StyledIframe>,
};
@observer
class Frame extends React.Component<Props> {
class Frame extends React.Component<PropsWithRef> {
mounted: boolean;
@observable isLoaded: boolean = false;
@@ -65,20 +68,20 @@ class Frame extends React.Component<Props> {
const Rounded = styled.div`
border-radius: 3px;
overflow: hidden;
width: ${props => props.width};
height: ${props => props.height};
width: ${(props) => props.width};
height: ${(props) => props.height};
`;
// This wrapper allows us to pass non-standard HTML attributes through to the DOM element
// https://www.styled-components.com/docs/basics#passed-props
const Iframe = props => <iframe {...props} />;
const Iframe = (props) => <iframe {...props} />;
const StyledIframe = styled(Iframe)`
border: 1px solid;
border-color: ${props => props.theme.embedBorder};
border-color: ${(props) => props.theme.embedBorder};
border-radius: 3px;
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
<Frame {...props} forwardedRef={ref} />
));
+7 -7
View File
@@ -1,14 +1,14 @@
// @flow
import { Provider } from "mobx-react";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "mobx-react";
import { BrowserRouter as Router } from "react-router-dom";
import stores from "stores";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
import ScrollToTop from "components/ScrollToTop";
import Toasts from "components/Toasts";
import Theme from "components/Theme";
import Toasts from "components/Toasts";
import Routes from "./routes";
import env from "env";
@@ -21,23 +21,23 @@ const element = document.getElementById("root");
if (element) {
render(
<React.Fragment>
<>
<ErrorBoundary>
<Provider {...stores}>
<Theme>
<Router>
<React.Fragment>
<>
<ScrollToTop>
<Routes />
</ScrollToTop>
<Toasts />
</React.Fragment>
</>
</Router>
</Theme>
</Provider>
</ErrorBoundary>
{DevTools && <DevTools position={{ bottom: 0, right: 0 }} />}
</React.Fragment>,
</>,
element
);
}
+8 -8
View File
@@ -1,16 +1,16 @@
// @flow
import * as React from "react";
import { Link } from "react-router-dom";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { SunIcon, MoonIcon } from "outline-icons";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import UiStore from "stores/UiStore";
import AuthStore from "stores/AuthStore";
import Flex from "components/Flex";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Modal from "components/Modal";
import UiStore from "stores/UiStore";
import KeyboardShortcuts from "scenes/KeyboardShortcuts";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Flex from "components/Flex";
import Modal from "components/Modal";
import {
developers,
changelog,
@@ -45,7 +45,7 @@ class AccountMenu extends React.Component<Props> {
const { ui } = this.props;
return (
<React.Fragment>
<>
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -118,7 +118,7 @@ class AccountMenu extends React.Component<Props> {
Log out
</DropdownMenuItem>
</DropdownMenu>
</React.Fragment>
</>
);
}
}
+89 -39
View File
@@ -1,20 +1,22 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import Modal from "components/Modal";
import VisuallyHidden from "components/VisuallyHidden";
import CollectionMembers from "scenes/CollectionMembers";
import { newDocumentUrl } from "utils/routeHelpers";
import getDataTransferFiles from "utils/getDataTransferFiles";
import importFile from "utils/importFile";
import Collection from "models/Collection";
import UiStore from "stores/UiStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import Collection from "models/Collection";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionExport from "scenes/CollectionExport";
import CollectionMembers from "scenes/CollectionMembers";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Modal from "components/Modal";
import VisuallyHidden from "components/VisuallyHidden";
import getDataTransferFiles from "utils/getDataTransferFiles";
import importFile from "utils/importFile";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
position?: "left" | "right" | "center",
@@ -30,7 +32,10 @@ type Props = {
@observer
class CollectionMenu extends React.Component<Props> {
file: ?HTMLInputElement;
@observable membersModalOpen: boolean = false;
@observable showCollectionMembers = false;
@observable showCollectionEdit = false;
@observable showCollectionDelete = false;
@observable showCollectionExport = false;
onNewDocument = (ev: SyntheticEvent<>) => {
ev.preventDefault();
@@ -61,31 +66,40 @@ class CollectionMenu extends React.Component<Props> {
}
};
onEdit = (ev: SyntheticEvent<>) => {
handleEditCollectionOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { collection } = this.props;
this.props.ui.setActiveModal("collection-edit", { collection });
this.showCollectionEdit = true;
};
onDelete = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { collection } = this.props;
this.props.ui.setActiveModal("collection-delete", { collection });
handleEditCollectionClose = () => {
this.showCollectionEdit = false;
};
onExport = (ev: SyntheticEvent<>) => {
handleDeleteCollectionOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
const { collection } = this.props;
this.props.ui.setActiveModal("collection-export", { collection });
this.showCollectionDelete = true;
};
onPermissions = (ev: SyntheticEvent<>) => {
handleDeleteCollectionClose = () => {
this.showCollectionDelete = false;
};
handleExportCollectionOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.membersModalOpen = true;
this.showCollectionExport = true;
};
handleExportCollectionClose = () => {
this.showCollectionExport = false;
};
handleMembersModalOpen = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.showCollectionMembers = true;
};
handleMembersModalClose = () => {
this.membersModalOpen = false;
this.showCollectionMembers = false;
};
render() {
@@ -93,13 +107,13 @@ class CollectionMenu extends React.Component<Props> {
const can = policies.abilities(collection.id);
return (
<React.Fragment>
<>
<VisuallyHidden>
<input
type="file"
ref={ref => (this.file = ref)}
ref={(ref) => (this.file = ref)}
onChange={this.onFilePicked}
onClick={ev => ev.stopPropagation()}
onClick={(ev) => ev.stopPropagation()}
accept="text/markdown, text/plain"
/>
</VisuallyHidden>
@@ -107,17 +121,17 @@ class CollectionMenu extends React.Component<Props> {
<Modal
title="Collection permissions"
onRequestClose={this.handleMembersModalClose}
isOpen={this.membersModalOpen}
isOpen={this.showCollectionMembers}
>
<CollectionMembers
collection={collection}
onSubmit={this.handleMembersModalClose}
onEdit={this.onEdit}
handleEditCollectionOpen={this.handleEditCollectionOpen}
/>
</Modal>
<DropdownMenu onOpen={onOpen} onClose={onClose} position={position}>
{collection && (
<React.Fragment>
<>
{can.update && (
<DropdownMenuItem onClick={this.onNewDocument}>
New document
@@ -130,29 +144,65 @@ class CollectionMenu extends React.Component<Props> {
)}
{can.update && <hr />}
{can.update && (
<DropdownMenuItem onClick={this.onEdit}>Edit</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleEditCollectionOpen}>
Edit
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.onPermissions}>
<DropdownMenuItem onClick={this.handleMembersModalOpen}>
Permissions
</DropdownMenuItem>
)}
{can.export && (
<DropdownMenuItem onClick={this.onExport}>
<DropdownMenuItem onClick={this.handleExportCollectionOpen}>
Export
</DropdownMenuItem>
)}
</React.Fragment>
</>
)}
{can.delete && (
<DropdownMenuItem onClick={this.onDelete}>Delete</DropdownMenuItem>
<DropdownMenuItem onClick={this.handleDeleteCollectionOpen}>
Delete
</DropdownMenuItem>
)}
</DropdownMenu>
</React.Fragment>
<Modal
title="Edit collection"
isOpen={this.showCollectionEdit}
onRequestClose={this.handleEditCollectionClose}
>
<CollectionEdit
onSubmit={this.handleEditCollectionClose}
collection={collection}
/>
</Modal>
<Modal
title="Delete collection"
isOpen={this.showCollectionDelete}
onRequestClose={this.handleDeleteCollectionClose}
>
<CollectionDelete
onSubmit={this.handleDeleteCollectionClose}
collection={collection}
/>
</Modal>
<Modal
title="Export collection"
isOpen={this.showCollectionExport}
onRequestClose={this.handleExportCollectionClose}
>
<CollectionExport
onSubmit={this.handleExportCollectionClose}
collection={collection}
/>
</Modal>
</>
);
}
}
export default inject("ui", "documents", "policies")(
withRouter(CollectionMenu)
);
export default inject(
"ui",
"documents",
"policies"
)(withRouter(CollectionMenu));
+36 -21
View File
@@ -1,17 +1,19 @@
// @flow
import * as React from "react";
import { Redirect } from "react-router-dom";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Redirect } from "react-router-dom";
import Document from "models/Document";
import UiStore from "stores/UiStore";
import AuthStore from "stores/AuthStore";
import CollectionStore from "stores/CollectionsStore";
import PoliciesStore from "stores/PoliciesStore";
import Modal from "components/Modal";
import UiStore from "stores/UiStore";
import Document from "models/Document";
import DocumentDelete from "scenes/DocumentDelete";
import DocumentShare from "scenes/DocumentShare";
import DocumentTemplatize from "scenes/DocumentTemplatize";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Modal from "components/Modal";
import {
documentUrl,
documentMoveUrl,
@@ -19,7 +21,6 @@ import {
documentHistoryUrl,
newDocumentUrl,
} from "utils/routeHelpers";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
ui: UiStore,
@@ -40,8 +41,9 @@ type Props = {
@observer
class DocumentMenu extends React.Component<Props> {
@observable redirectTo: ?string;
@observable showDeleteModal: boolean = false;
@observable showTemplateModal: boolean = false;
@observable showDeleteModal = false;
@observable showTemplateModal = false;
@observable showShareModal = false;
componentDidUpdate() {
this.redirectTo = undefined;
@@ -129,7 +131,11 @@ class DocumentMenu extends React.Component<Props> {
handleShareLink = async (ev: SyntheticEvent<>) => {
const { document } = this.props;
await document.share();
this.props.ui.setActiveModal("document-share", { document });
this.showShareModal = true;
};
handleCloseShareModal = () => {
this.showShareModal = false;
};
render() {
@@ -153,7 +159,7 @@ class DocumentMenu extends React.Component<Props> {
const canViewHistory = can.read && !can.restore;
return (
<React.Fragment>
<>
<DropdownMenu
className={className}
position={position}
@@ -197,7 +203,7 @@ class DocumentMenu extends React.Component<Props> {
</DropdownMenuItem>
)}
{showToggleEmbeds && (
<React.Fragment>
<>
{document.embedsDisabled ? (
<DropdownMenuItem onClick={document.enableEmbeds}>
Enable embeds
@@ -207,7 +213,7 @@ class DocumentMenu extends React.Component<Props> {
Disable embeds
</DropdownMenuItem>
)}
</React.Fragment>
</>
)}
{!can.restore && <hr />}
@@ -219,12 +225,11 @@ class DocumentMenu extends React.Component<Props> {
New nested document
</DropdownMenuItem>
)}
{can.update &&
!document.isTemplate && (
<DropdownMenuItem onClick={this.handleOpenTemplateModal}>
Create template
</DropdownMenuItem>
)}
{can.update && !document.isTemplate && (
<DropdownMenuItem onClick={this.handleOpenTemplateModal}>
Create template
</DropdownMenuItem>
)}
{can.update && (
<DropdownMenuItem onClick={this.handleEdit}>Edit</DropdownMenuItem>
)}
@@ -248,11 +253,11 @@ class DocumentMenu extends React.Component<Props> {
)}
<hr />
{canViewHistory && (
<React.Fragment>
<>
<DropdownMenuItem onClick={this.handleDocumentHistory}>
History
</DropdownMenuItem>
</React.Fragment>
</>
)}
{can.download && (
<DropdownMenuItem onClick={this.handleExport}>
@@ -283,7 +288,17 @@ class DocumentMenu extends React.Component<Props> {
onSubmit={this.handleCloseTemplateModal}
/>
</Modal>
</React.Fragment>
<Modal
title="Share document"
onRequestClose={this.handleCloseShareModal}
isOpen={this.showShareModal}
>
<DocumentShare
document={this.props.document}
onSubmit={this.handleCloseShareModal}
/>
</Modal>
</>
);
}
}
+11 -11
View File
@@ -1,16 +1,16 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import Modal from "components/Modal";
import GroupEdit from "scenes/GroupEdit";
import GroupDelete from "scenes/GroupDelete";
import Group from "models/Group";
import UiStore from "stores/UiStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import Group from "models/Group";
import GroupDelete from "scenes/GroupDelete";
import GroupEdit from "scenes/GroupEdit";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import Modal from "components/Modal";
type Props = {
ui: UiStore,
@@ -50,7 +50,7 @@ class GroupMenu extends React.Component<Props> {
const can = policies.abilities(group.id);
return (
<React.Fragment>
<>
<Modal
title="Edit group"
onRequestClose={this.handleEditModalClose}
@@ -75,7 +75,7 @@ class GroupMenu extends React.Component<Props> {
<DropdownMenu onOpen={onOpen} onClose={onClose}>
{group && (
<React.Fragment>
<>
<DropdownMenuItem onClick={this.props.onMembers}>
Members
</DropdownMenuItem>
@@ -91,10 +91,10 @@ class GroupMenu extends React.Component<Props> {
Delete
</DropdownMenuItem>
)}
</React.Fragment>
</>
)}
</DropdownMenu>
</React.Fragment>
</>
);
}
}
+4 -4
View File
@@ -1,14 +1,14 @@
// @flow
import * as React from "react";
import { Redirect } from "react-router-dom";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { MoreIcon } from "outline-icons";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { newDocumentUrl } from "utils/routeHelpers";
import Document from "models/Document";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
label?: React.Node,
+8 -7
View File
@@ -1,21 +1,21 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { Redirect } from "react-router-dom";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { newDocumentUrl } from "utils/routeHelpers";
import CollectionsStore from "stores/CollectionsStore";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
import {
DropdownMenu,
DropdownMenuItem,
Header,
} from "components/DropdownMenu";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
label?: React.Node,
@@ -63,7 +63,7 @@ class NewDocumentMenu extends React.Component<Props> {
{...rest}
>
<Header>Choose a collection</Header>
{collections.orderedData.map(collection => {
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return (
@@ -72,7 +72,8 @@ class NewDocumentMenu extends React.Component<Props> {
onClick={() => this.handleNewDocument(collection.id)}
disabled={!can.update}
>
<CollectionIcon collection={collection} />&nbsp;{collection.name}
<CollectionIcon collection={collection} />
&nbsp;{collection.name}
</DropdownMenuItem>
);
})}
+8 -7
View File
@@ -1,20 +1,20 @@
// @flow
import * as React from "react";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import { Redirect } from "react-router-dom";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { newDocumentUrl } from "utils/routeHelpers";
import CollectionsStore from "stores/CollectionsStore";
import PoliciesStore from "stores/PoliciesStore";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
import {
DropdownMenu,
DropdownMenuItem,
Header,
} from "components/DropdownMenu";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
label?: React.Node,
@@ -53,7 +53,7 @@ class NewTemplateMenu extends React.Component<Props> {
{...rest}
>
<Header>Choose a collection</Header>
{collections.orderedData.map(collection => {
{collections.orderedData.map((collection) => {
const can = policies.abilities(collection.id);
return (
@@ -62,7 +62,8 @@ class NewTemplateMenu extends React.Component<Props> {
onClick={() => this.handleNewDocument(collection.id)}
disabled={!can.update}
>
<CollectionIcon collection={collection} />&nbsp;{collection.name}
<CollectionIcon collection={collection} />
&nbsp;{collection.name}
</DropdownMenuItem>
);
})}
+4 -4
View File
@@ -1,14 +1,14 @@
// @flow
import { inject } from "mobx-react";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import { inject } from "mobx-react";
import UiStore from "stores/UiStore";
import Document from "models/Document";
import Revision from "models/Revision";
import CopyToClipboard from "components/CopyToClipboard";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import { documentHistoryUrl } from "utils/routeHelpers";
import Revision from "models/Revision";
import Document from "models/Document";
import UiStore from "stores/UiStore";
type Props = {
onOpen?: () => void,
+4 -4
View File
@@ -1,14 +1,14 @@
// @flow
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Redirect } from "react-router-dom";
import { inject, observer } from "mobx-react";
import { observable } from "mobx";
import CopyToClipboard from "components/CopyToClipboard";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import SharesStore from "stores/SharesStore";
import UiStore from "stores/UiStore";
import Share from "models/Share";
import CopyToClipboard from "components/CopyToClipboard";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
onOpen?: () => void,
+2 -2
View File
@@ -1,7 +1,7 @@
// @flow
import * as React from "react";
import { observer, inject } from "mobx-react";
import { DocumentIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import DocumentsStore from "stores/DocumentsStore";
import Document from "models/Document";
@@ -33,7 +33,7 @@ class TemplatesMenu extends React.Component<Props> {
}
{...rest}
>
{templates.map(template => (
{templates.map((template) => (
<DropdownMenuItem
key={template.id}
onClick={() => document.updateFromTemplate(template)}
+8 -11
View File
@@ -1,10 +1,10 @@
// @flow
import * as React from "react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
import UsersStore from "stores/UsersStore";
import User from "models/User";
import { DropdownMenu, DropdownMenuItem } from "components/DropdownMenu";
type Props = {
user: User,
@@ -18,9 +18,7 @@ class UserMenu extends React.Component<Props> {
const { user, users } = this.props;
if (
!window.confirm(
`Are you want to make ${
user.name
} an admin? Admins can modify team and billing information.`
`Are you want to make ${user.name} an admin? Admins can modify team and billing information.`
)
) {
return;
@@ -72,12 +70,11 @@ class UserMenu extends React.Component<Props> {
Make {user.name} a member
</DropdownMenuItem>
)}
{!user.isAdmin &&
!user.isSuspended && (
<DropdownMenuItem onClick={this.handlePromote}>
Make {user.name} an admin
</DropdownMenuItem>
)}
{!user.isAdmin && !user.isSuspended && (
<DropdownMenuItem onClick={this.handlePromote}>
Make {user.name} an admin
</DropdownMenuItem>
)}
{!user.lastActiveAt && (
<DropdownMenuItem onClick={this.handleRevoke}>
Revoke invite
+3 -3
View File
@@ -11,7 +11,7 @@ export default class BaseModel {
this.store = store;
}
save = async params => {
save = async (params: ?Object) => {
this.isSaving = true;
try {
@@ -27,7 +27,7 @@ export default class BaseModel {
}
};
fetch = (options: *) => {
fetch = (options?: any) => {
return this.store.fetch(this.id, options);
};
@@ -44,7 +44,7 @@ export default class BaseModel {
}
};
toJS = () => {
toJS = (): Object => {
return { ...this };
};
}
+6 -6
View File
@@ -3,8 +3,8 @@ import { pick } from "lodash";
import { action, computed, observable } from "mobx";
import BaseModel from "models/BaseModel";
import Document from "models/Document";
import { client } from "utils/ApiClient";
import type { NavigationNode } from "types";
import { client } from "utils/ApiClient";
export default class Collection extends BaseModel {
@observable isSaving: boolean;
@@ -37,7 +37,7 @@ export default class Collection extends BaseModel {
get documentIds(): string[] {
const results = [];
const travelDocuments = (documentList, path) =>
documentList.forEach(document => {
documentList.forEach((document) => {
results.push(document.id);
travelDocuments(document.children);
});
@@ -49,7 +49,7 @@ export default class Collection extends BaseModel {
@action
updateDocument(document: Document) {
const travelDocuments = (documentList, path) =>
documentList.forEach(d => {
documentList.forEach((d) => {
if (d.id === document.id) {
d.title = document.title;
d.url = document.url;
@@ -63,8 +63,8 @@ export default class Collection extends BaseModel {
getDocumentChildren(documentId: string): NavigationNode[] {
let result = [];
const traveler = nodes => {
nodes.forEach(childNode => {
const traveler = (nodes) => {
nodes.forEach((childNode) => {
if (childNode.id === documentId) {
result = childNode.children;
return;
@@ -83,7 +83,7 @@ export default class Collection extends BaseModel {
pathToDocument(document: Document) {
let path;
const traveler = (nodes, previousPath) => {
nodes.forEach(childNode => {
nodes.forEach((childNode) => {
const newPath = [...previousPath, childNode];
if (childNode.id === document.id) {
path = newPath;

Some files were not shown because too many files have changed in this diff Show More