Compare commits

...

28 Commits

Author SHA1 Message Date
Tom Moor 2aaad03270 refactor 2020-08-20 19:05:48 -07:00
Tom Moor 9252683260 fix: SocketPresence account for socket changing 2020-08-20 00:06:51 -07:00
Tom Moor f5748eb5e7 check connection on page visibility change 2020-08-19 22:51:18 -07:00
Tom Moor 0555fd2caa pref: JS bundling improvements (#1461)
* perf: Split only initial vendors
2020-08-17 22:09:12 -07:00
Tom Moor d885252fb0 fix: Mobile style fixes and improvements (#1459)
* fixes #1457 – check for matchMedia function before using it

* fixes: Depth issues
closes #1458

* fixes: Long breadcrumbs cause horizontal overflow

* fix: Improve tabs and overflow on mobile
2020-08-17 00:08:22 -07:00
Tom Moor df9b0bcf91 fix: Websocket reconnect when navigating from settings -> home 2020-08-14 17:47:12 -07:00
Tom Moor 31910f1628 Remove auto reconnect, increase reconnectionDelayMax 2020-08-14 17:25:55 -07:00
Tom Moor 14cb3a36c1 perf: Reduce initial bundle size / async bundle loading (#1456)
* feat: Move to React.lazy

* perf: Remove duplicate babel/runtime

* fix: Run yarn-deduplicate

* Further attempts to remove rich-markdown-editor from initial chunk

* perf: Lazy loading of authenticated routes

* perf: Move color picker to async loading
fix: Display placeholder when loading rich editor

* fix: Cache bust on auto reload
2020-08-14 17:23:58 -07:00
Tom Moor d3350c20b6 perf: Attempt websocket connection before polling 2020-08-14 13:37:11 -07:00
Tom Moor 174acfac32 fix: Unnecessary shares.info request when loading public share (#1453)
closes #1450
2020-08-13 16:48:03 -07:00
Tom Moor 9ef4e2b437 Update LICENSE 2020-08-12 20:04:48 -07:00
Tom Moor 8088da8cf3 0.46.0 2020-08-12 20:03:57 -07:00
Tom Moor 221ee48429 fix: Don't mangle class names in production 2020-08-12 19:28:15 -07:00
Tom Moor ffe8c046ef fix: Bump RME – Improves floating toolbar behavior 2020-08-12 17:01:27 -07:00
Tom Moor dbe8a10702 fix: Login to X should be centered when team name wraps to newline 2020-08-12 14:05:32 -07:00
Tom Moor 11f7e3a060 chore: Bundle Stats / Webpack v4 (#1448)
* chore: Experiment with bundle size monitoring service

* chore: Ensure build runs on CI, move lint and flow before test

* chore: Upgrade Webpack v3 -> v4

* chore: Add webpack-cli
Remove unused dep
Move deps to dev

* Move babel deps to production

* Move babel deps to production
2020-08-12 13:16:10 -07:00
Tom Moor 0f41a04e49 refactor: Remove centralized Modal management (#1444)
* refactor: Finally remove centralized Modals component

* chore: Cleanup related unused methods in UiStore
2020-08-12 10:49:15 -07:00
Tom Moor d055021ad4 chore: Remove all usage of collection.type (#1445)
* chore: Remove all usage of collection.type

* migration: Remove type column
2020-08-12 10:49:02 -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
117 changed files with 2109 additions and 1827 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"version": "2",
"proposals": true
},
"useBuiltIns": "usage",
"useBuiltIns": "usage"
}
]
],
+7 -4
View File
@@ -29,12 +29,15 @@ jobs:
- run:
name: migrate
command: ./node_modules/.bin/sequelize db:migrate --url $DATABASE_URL_TEST
- run:
name: test
command: yarn test
- run:
name: lint
command: yarn lint
- run:
name: flow
command: yarn flow check --max-workers 4
command: yarn flow check --max-workers 4
- run:
name: test
command: yarn test
- run:
name: build
command: yarn build
+3
View File
@@ -97,5 +97,8 @@
},
"env": {
"jest": true
},
"globals": {
"EDITOR_VERSION": true
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.44.0
Licensed Work: Outline 0.46.0
The Licensed Work is (c) 2020 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2023-07-03
Change Date: 2023-08-12
Change License: Apache License, Version 2.0
+2 -2
View File
@@ -40,7 +40,7 @@ class AvatarWithPresence extends React.Component<Props> {
} = this.props;
return (
<React.Fragment>
<>
<Tooltip
tooltip={
<Centered>
@@ -69,7 +69,7 @@ class AvatarWithPresence extends React.Component<Props> {
isOpen={this.isOpen}
onRequestClose={this.handleCloseProfile}
/>
</React.Fragment>
</>
);
}
}
+12 -12
View File
@@ -33,11 +33,11 @@ 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) => (
@@ -46,7 +46,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
{n.title}
</React.Fragment>
))}
</React.Fragment>
</>
);
}
@@ -59,24 +59,24 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<React.Fragment>
<>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />
&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
{isDraft && (
<React.Fragment>
<>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />
&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</React.Fragment>
</>
)}
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />
@@ -84,17 +84,17 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
<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>
);
+1 -1
View File
@@ -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} />
));
+1
View File
@@ -9,6 +9,7 @@ type Props = {
const Container = styled.div`
width: 100%;
max-width: 100vw;
padding: 60px 20px;
${breakpoint("tablet")`
+3 -2
View File
@@ -10,6 +10,7 @@ export type Props = {
labelHidden?: boolean,
className?: string,
note?: string,
short?: boolean,
small?: boolean,
};
@@ -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>
</>
);
}
+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 = 250, 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;
}
+111 -27
View File
@@ -1,39 +1,123 @@
// @flow
import { inject } from "mobx-react";
import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
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);
@@ -7,9 +7,9 @@ import styled, { withTheme } from "styled-components";
import Document from "models/Document";
import Badge from "components/Badge";
import Button from "components/Button";
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 { newDocumentUrl } from "utils/routeHelpers";
@@ -129,7 +129,7 @@ class DocumentPreview extends React.Component<Props> {
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
<DocumentMeta
document={document}
showCollection={showCollection}
showPublished={showPublished}
@@ -169,6 +169,7 @@ const DocumentLink = styled(Link)`
border-radius: 8px;
max-height: 50vh;
min-width: 100%;
max-width: calc(100vw - 40px);
overflow: hidden;
position: relative;
+4 -4
View File
@@ -161,7 +161,7 @@ class DropdownMenu extends React.Component<Props> {
closeOnEsc
>
{({ closePortal, openPortal, isOpen, portal }) => (
<React.Fragment>
<>
<Label
onMouseMove={hover ? this.clearCloseTimeout : undefined}
onMouseOut={
@@ -220,7 +220,7 @@ class DropdownMenu extends React.Component<Props> {
</Menu>
</Position>
)}
</React.Fragment>
</>
)}
</PortalWithState>
</div>
@@ -232,7 +232,7 @@ const Label = styled(Flex).attrs({
justify: "center",
align: "center",
})`
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
cursor: pointer;
`;
@@ -244,7 +244,7 @@ const Position = styled.div`
${({ top }) => (top !== undefined ? `top: ${top}px` : "")};
${({ bottom }) => (bottom !== undefined ? `bottom: ${bottom}px` : "")};
max-height: 75%;
z-index: 1000;
z-index: ${(props) => props.theme.depths.menu};
transform: ${(props) =>
props.position === "center" ? "translateX(-50%)" : "initial"};
pointer-events: none;
@@ -26,12 +26,12 @@ const DropdownMenuItem = ({
{...rest}
>
{selected !== undefined && (
<React.Fragment>
<>
<CheckmarkIcon
color={selected === false ? "transparent" : undefined}
/>
&nbsp;
</React.Fragment>
</>
)}
{children}
</MenuItem>
+25 -21
View File
@@ -1,34 +1,34 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import { lighten } from "polished";
import * as React from "react";
import { withRouter, type RouterHistory } from "react-router-dom";
import RichMarkdownEditor from "rich-markdown-editor";
import styled, { withTheme } from "styled-components";
import UiStore from "stores/UiStore";
import ErrorBoundary from "components/ErrorBoundary";
import Tooltip from "components/Tooltip";
import embeds from "../embeds";
import isInternalUrl from "utils/isInternalUrl";
import { uploadFile } from "utils/uploadFile";
const RichMarkdownEditor = React.lazy(() => import("rich-markdown-editor"));
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<any>,
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,20 +62,24 @@ class Editor extends React.Component<Props> {
};
onShowToast = (message: string) => {
this.props.ui.showToast(message);
if (this.props.ui) {
this.props.ui.showToast(message);
}
};
render() {
return (
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
{...this.props}
/>
<ErrorBoundary reloadOnChunkMissing>
<StyledEditor
ref={this.props.forwardedRef}
uploadImage={this.onUploadImage}
onClickLink={this.onClickLink}
onShowToast={this.onShowToast}
embeds={this.props.disableEmbeds ? EMPTY_ARRAY : embeds}
tooltip={EditorTooltip}
{...this.props}
/>
</ErrorBoundary>
);
}
}
@@ -120,6 +124,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} />
));
+16 -2
View File
@@ -8,9 +8,11 @@ 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,
reloadOnChunkMissing?: boolean,
};
@observer
@@ -22,13 +24,25 @@ class ErrorBoundary extends React.Component<Props> {
this.error = error;
console.error(error);
if (
this.props.reloadOnChunkMissing &&
error.message &&
error.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(true);
return;
}
if (window.Sentry) {
window.Sentry.captureException(error);
}
}
handleReload = () => {
window.location.reload();
window.location.reload(true);
};
handleShowDetails = () => {
@@ -41,7 +55,7 @@ class ErrorBoundary extends React.Component<Props> {
render() {
if (this.error) {
const isReported = !!window.Sentry;
const isReported = !!window.Sentry && env.DEPLOYMENT === "hosted";
return (
<CenteredContent>
+24
View File
@@ -0,0 +1,24 @@
// @flow
import * as React from "react";
import styled from "styled-components";
import Empty from "components/Empty";
import Fade from "components/Fade";
import Flex from "components/Flex";
export default function FullscreenLoading() {
return (
<Fade timing={500}>
<Centered>
<Empty>Loading</Empty>
</Centered>
</Fade>
);
}
const Centered = styled(Flex)`
text-align: center;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
`;
+4 -4
View File
@@ -46,15 +46,15 @@ class GroupListItem extends React.Component<Props> {
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>
</>
);
}
}
+1
View File
@@ -4,6 +4,7 @@ import styled from "styled-components";
const Heading = styled.h1`
display: flex;
align-items: center;
${(props) => (props.centered ? "text-align: center;" : "")}
svg {
margin-left: -6px;
+6 -2
View File
@@ -5,7 +5,7 @@ import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components";
import { fadeAndSlideIn } from "shared/styles/animations";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import HoverPreviewDocument from "components/HoverPreviewDocument";
import isInternalUrl from "utils/isInternalUrl";
@@ -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();
@@ -68,6 +68,8 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
if (cardRef.current) {
cardRef.current.addEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.addEventListener("mouseleave", startCloseTimer);
}
@@ -82,6 +84,8 @@ function HoverPreview({ node, documents, onClose, event }: Props) {
if (cardRef.current) {
cardRef.current.removeEventListener("mouseenter", stopCloseTimer);
}
if (cardRef.current) {
cardRef.current.removeEventListener("mouseleave", startCloseTimer);
}
+11 -9
View File
@@ -3,9 +3,9 @@ import { inject, observer } from "mobx-react";
import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentIds";
import { parseDocumentSlugFromUrl } from "shared/utils/parseDocumentSlug";
import DocumentsStore from "stores/DocumentsStore";
import DocumentMeta from "components/DocumentMeta";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
type Props = {
@@ -27,14 +27,16 @@ 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}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
<React.Suspense fallback={<div />}>
<Editor
key={document.id}
defaultValue={document.getSummary()}
disableEmbeds
readOnly
/>
</React.Suspense>
</Content>
);
}
+19 -9
View File
@@ -22,13 +22,17 @@ import {
VehicleIcon,
} from "outline-icons";
import * as React from "react";
import { TwitterPicker } from "react-color";
import styled from "styled-components";
import { DropdownMenu } from "components/DropdownMenu";
import Flex from "components/Flex";
import HelpText from "components/HelpText";
import { LabelText } from "components/Input";
import NudeButton from "components/NudeButton";
const TwitterPicker = React.lazy(() =>
import("react-color/lib/components/twitter/Twitter")
);
export const icons = {
collection: {
component: CollectionIcon,
@@ -193,14 +197,16 @@ class IconPicker extends React.Component<Props> {
})}
</Icons>
<Flex onClick={preventEventBubble}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
triangle="hide"
/>
<React.Suspense fallback={<Loading>Loading</Loading>}>
<ColorPicker
color={this.props.color}
onChange={(color) =>
this.props.onChange(color.hex, this.props.icon)
}
colors={colors}
triangle="hide"
/>
</React.Suspense>
</Flex>
</DropdownMenu>
</Wrapper>
@@ -226,6 +232,10 @@ const IconButton = styled(NudeButton)`
height: 30px;
`;
const Loading = styled(HelpText)`
padding: 16px;
`;
const ColorPicker = styled(TwitterPicker)`
box-shadow: none !important;
background: transparent !important;
+13 -29
View File
@@ -1,8 +1,11 @@
// @flow
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 Editor from "components/Editor";
import HelpText from "components/HelpText";
import { LabelText, Outline } from "components/Input";
type Props = {
@@ -10,6 +13,7 @@ type Props = {
minHeight?: number,
maxHeight?: number,
readOnly?: boolean,
ui: UiStore,
};
@observer
@@ -17,10 +21,6 @@ class InputRich extends React.Component<Props> {
@observable editorComponent: React.ComponentType<any>;
@observable focused: boolean = false;
componentDidMount() {
this.loadEditor();
}
handleBlur = () => {
this.focused = false;
};
@@ -29,50 +29,34 @@ class InputRich extends React.Component<Props> {
this.focused = true;
};
loadEditor = async () => {
try {
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();
}
};
render() {
const { label, minHeight, maxHeight, ...rest } = this.props;
const Editor = this.editorComponent;
const { label, minHeight, maxHeight, ui, ...rest } = this.props;
return (
<React.Fragment>
<>
<LabelText>{label}</LabelText>
<StyledOutline
maxHeight={maxHeight}
minHeight={minHeight}
focused={this.focused}
>
{Editor ? (
<React.Suspense fallback={<HelpText>Loading editor</HelpText>}>
<Editor
onBlur={this.handleBlur}
onFocus={this.handleFocus}
ui={ui}
grow
{...rest}
/>
) : (
"Loading…"
)}
</React.Suspense>
</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 +67,4 @@ const StyledOutline = styled(Outline)`
}
`;
export default withTheme(InputRich);
export default inject("ui")(withTheme(InputRich));
-2
View File
@@ -19,7 +19,6 @@ import Flex from "components/Flex";
import { LoadingIndicatorBar } from "components/LoadingIndicator";
import Modal from "components/Modal";
import Modals from "components/Modals";
import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import {
@@ -127,7 +126,6 @@ class Layout extends React.Component<Props> {
/>
</Switch>
</Container>
<Modals ui={ui} />
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -18,7 +18,7 @@ const loadingFrame = keyframes`
const Container = styled.div`
position: fixed;
top: 0;
z-index: 9999;
z-index: ${(props) => props.theme.depths.loadingIndicatorBar};
background-color: #03a9f4;
width: 100%;
@@ -1,21 +1,24 @@
// @flow
import * as React from "react";
import styled from "styled-components";
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>
);
}
+4 -4
View File
@@ -23,7 +23,7 @@ const GlobalStyles = createGlobalStyle`
.ReactModal__Overlay {
background-color: ${(props) =>
transparentize(0.25, props.theme.background)} !important;
z-index: 100;
z-index: ${(props) => props.theme.depths.modalOverlay};
}
${breakpoint("tablet")`
@@ -64,7 +64,7 @@ const Modal = ({
if (!isOpen) return null;
return (
<React.Fragment>
<>
<GlobalStyles />
<StyledModal
contentLabel={title}
@@ -85,7 +85,7 @@ const Modal = ({
<CloseIcon size={32} color="currentColor" />
</Close>
</StyledModal>
</React.Fragment>
</>
);
};
@@ -103,7 +103,7 @@ const StyledModal = styled(ReactModal)`
left: 0;
bottom: 0;
right: 0;
z-index: 100;
z-index: ${(props) => props.theme.depths.modal};
display: flex;
justify-content: center;
align-items: flex-start;
-58
View File
@@ -1,58 +0,0 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import UiStore from "stores/UiStore";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionExport from "scenes/CollectionExport";
import CollectionNew from "scenes/CollectionNew";
import DocumentShare from "scenes/DocumentShare";
import BaseModal from "components/Modal";
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;
+1 -1
View File
@@ -20,6 +20,6 @@ const Button = styled.button`
}
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<any, typeof Button>((props, ref) => (
<Button {...props} ref={ref} />
));
+11 -17
View File
@@ -4,8 +4,8 @@ import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Waypoint } from "react-waypoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DelayedMount from "components/DelayedMount";
import { ListPlaceholder } from "components/LoadingPlaceholder";
type Props = {
@@ -20,8 +20,6 @@ type Props = {
@observer
class PaginatedList extends React.Component<Props> {
isInitiallyLoaded: boolean = false;
timeout: ?TimeoutID;
@observable isLongLoading: boolean = false;
@observable isLoaded: boolean = false;
@observable isFetchingMore: boolean = false;
@observable isFetching: boolean = false;
@@ -36,11 +34,6 @@ class PaginatedList extends React.Component<Props> {
componentDidMount() {
this.fetchResults();
this.timeout = setTimeout(() => (this.isLongLoading = true), 200);
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
componentDidUpdate(prevProps: Props) {
@@ -97,19 +90,16 @@ class PaginatedList extends React.Component<Props> {
const { items, heading, empty } = this.props;
const showLoading =
this.isFetching &&
this.isLongLoading &&
!this.isFetchingMore &&
!this.isInitiallyLoaded;
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
const showEmpty = !items.length && !showLoading;
const showList =
(this.isLoaded || this.isInitiallyLoaded) && !showLoading && !showEmpty;
return (
<React.Fragment>
<>
{showEmpty && empty}
{showList && (
<React.Fragment>
<>
{heading}
<ArrowKeyNavigation
mode={ArrowKeyNavigation.mode.VERTICAL}
@@ -120,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>
)}
</>
);
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ const StyledPopover = styled(BoundlessPopover)`
position: absolute;
top: 0;
left: 0;
z-index: 9999;
z-index: ${(props) => props.theme.depths.popover};
svg {
height: 16px;
-120
View File
@@ -1,120 +0,0 @@
// @flow
import { inject, observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import Document from "models/Document";
import Breadcrumb from "components/Breadcrumb";
import Flex from "components/Flex";
import Time from "components/Time";
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));
+20 -7
View File
@@ -17,7 +17,7 @@ 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 CollectionNew from "scenes/CollectionNew";
import Invite from "scenes/Invite";
import Flex from "components/Flex";
import Modal from "components/Modal";
@@ -34,21 +34,25 @@ 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<>) => {
@@ -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);
+1 -1
View File
@@ -40,7 +40,7 @@ type Props = {
@observer
class SettingsSidebar extends React.Component<Props> {
returnToDashboard = () => {
this.props.history.push("/");
this.props.history.push("/home");
};
render() {
+1 -1
View File
@@ -71,7 +71,7 @@ const Container = styled(Flex)`
transition: left 100ms ease-out,
${(props) => props.theme.backgroundTransition};
margin-left: ${(props) => (props.mobileSidebarVisible ? 0 : "-100%")};
z-index: 1000;
z-index: ${(props) => props.theme.depths.sidebar};
@media print {
display: none;
@@ -55,7 +55,7 @@ class Collections extends React.Component<Props> {
const { collections, ui, documents } = this.props;
const content = (
<React.Fragment>
<>
{collections.orderedData.map((collection) => (
<CollectionLink
key={collection.id}
@@ -73,7 +73,7 @@ class Collections extends React.Component<Props> {
label="New collection…"
exact
/>
</React.Fragment>
</>
);
return (
@@ -1,16 +1,15 @@
// @flow
import { ExpandedIcon } from "outline-icons";
import * as React from "react";
import styled, { withTheme } from "styled-components";
import styled from "styled-components";
import Flex from "components/Flex";
import TeamLogo from "components/TeamLogo";
type Props = {
teamName: string,
subheading: string,
subheading: React.Node,
showDisclosure?: boolean,
logoUrl: string,
theme: Object,
};
function HeaderBlock({
@@ -18,7 +17,6 @@ function HeaderBlock({
teamName,
subheading,
logoUrl,
theme,
...rest
}: Props) {
return (
@@ -27,7 +25,7 @@ function HeaderBlock({
<Flex align="flex-start" column>
<TeamName showDisclosure>
{teamName}{" "}
{showDisclosure && <StyledExpandedIcon color={theme.text} />}
{showDisclosure && <StyledExpandedIcon color="currentColor" />}
</TeamName>
<Subheading>{subheading}</Subheading>
</Flex>
@@ -73,4 +71,4 @@ const Header = styled(Flex)`
}
`;
export default withTheme(HeaderBlock);
export default HeaderBlock;
+2 -2
View File
@@ -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>
</>
}
/>
);
+41 -11
View File
@@ -3,7 +3,7 @@ import { find } from "lodash";
import { observable } from "mobx";
import { inject, observer } from "mobx-react";
import * as React from "react";
import io from "socket.io-client";
import io, { Socket } from "socket.io-client";
import AuthStore from "stores/AuthStore";
import CollectionsStore from "stores/CollectionsStore";
import DocumentPresenceStore from "stores/DocumentPresenceStore";
@@ -13,6 +13,7 @@ import MembershipsStore from "stores/MembershipsStore";
import PoliciesStore from "stores/PoliciesStore";
import UiStore from "stores/UiStore";
import ViewsStore from "stores/ViewsStore";
import { getVisibilityListener, getPageVisible } from "utils/pageVisibility";
export const SocketContext: any = React.createContext();
@@ -31,12 +32,42 @@ type Props = {
@observer
class SocketProvider extends React.Component<Props> {
@observable socket;
@observable socket: Socket;
componentDidMount() {
this.createConnection();
document.addEventListener(getVisibilityListener(), this.checkConnection);
}
componentWillUnmount() {
if (this.socket) {
this.socket.authenticated = false;
this.socket.disconnect();
}
document.removeEventListener(getVisibilityListener(), this.checkConnection);
}
checkConnection = () => {
if (this.socket && this.socket.disconnected && getPageVisible()) {
// null-ifying this reference is important, do not remove. Without it
// references to old sockets are potentially held in context
this.socket.close();
this.socket = null;
this.createConnection();
}
};
createConnection = () => {
this.socket = io(window.location.origin, {
path: "/realtime",
transports: ["websocket"],
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
});
this.socket.authenticated = false;
const {
@@ -61,12 +92,18 @@ 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();
});
// on reconnection, reset the transports option, as the Websocket
// connection may have failed (caused by proxy, firewall, browser, ...)
this.socket.on("reconnect_attempt", () => {
this.socket.io.opts.transports = ["polling", "websocket"];
});
this.socket.on("authenticated", () => {
this.socket.authenticated = true;
});
@@ -254,14 +291,7 @@ class SocketProvider extends React.Component<Props> {
this.socket.on("user.presence", (event) => {
presence.touch(event.documentId, event.userId, event.isEditing);
});
}
componentWillUnmount() {
if (this.socket) {
this.socket.disconnect();
this.socket.authenticated = false;
}
}
};
render() {
return (
+2
View File
@@ -6,6 +6,8 @@ const Tabs = styled.nav`
border-bottom: 1px solid ${(props) => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;
overflow-y: auto;
white-space: nowrap;
`;
export const Separator = styled.span`
+2 -2
View File
@@ -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>
);
}
+1 -1
View File
@@ -34,7 +34,7 @@ const List = styled.ol`
list-style: none;
margin: 0;
padding: 0;
z-index: 1000;
z-index: ${(props) => props.theme.depths.toasts};
`;
export default inject("ui")(Toasts);
+1 -1
View File
@@ -3,7 +3,7 @@ import { darken } from "polished";
import * as React from "react";
import styled from "styled-components";
import { fadeAndScaleIn } from "shared/styles/animations";
import type { Toast as TToast } from "../../../types";
import type { Toast as TToast } from "types";
type Props = {
onRequestClose: () => void,
+2 -2
View File
@@ -24,9 +24,9 @@ class Tooltip extends React.Component<Props> {
if (shortcut) {
content = (
<React.Fragment>
<>
{tooltip} &middot; <Shortcut>{shortcut}</Shortcut>
</React.Fragment>
</>
);
}
+4 -21
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;
@@ -56,16 +41,14 @@ class Gist extends React.Component<Props> {
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%"
+6 -3
View File
@@ -7,13 +7,16 @@ 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;
@@ -79,6 +82,6 @@ const StyledIframe = styled(Iframe)`
border-radius: 3px;
`;
export default React.forwardRef((props, ref) => (
export default React.forwardRef<Props, typeof Frame>((props, ref) => (
<Frame {...props} forwardedRef={ref} />
));
+4 -4
View File
@@ -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
);
}
+2 -2
View File
@@ -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>
</>
);
}
}
+73 -25
View File
@@ -7,8 +7,10 @@ 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";
@@ -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,7 +107,7 @@ class CollectionMenu extends React.Component<Props> {
const can = policies.abilities(collection.id);
return (
<React.Fragment>
<>
<VisuallyHidden>
<input
type="file"
@@ -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,25 +144,59 @@ 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>
</>
);
}
}
+25 -9
View File
@@ -10,6 +10,7 @@ import PoliciesStore from "stores/PoliciesStore";
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";
@@ -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 />}
@@ -247,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}>
@@ -282,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>
</>
);
}
}
+4 -4
View File
@@ -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>
</>
);
}
}
+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 };
};
}
-1
View File
@@ -16,7 +16,6 @@ export default class Collection extends BaseModel {
icon: string;
color: string;
private: boolean;
type: "atlas" | "journal";
documents: NavigationNode[];
createdAt: ?string;
updatedAt: ?string;
-121
View File
@@ -1,121 +0,0 @@
// @flow
import * as React from "react";
import { Switch, Route, Redirect, type Match } from "react-router-dom";
import Archive from "scenes/Archive";
import Collection from "scenes/Collection";
import Dashboard from "scenes/Dashboard";
import KeyedDocument from "scenes/Document/KeyedDocument";
import DocumentNew from "scenes/DocumentNew";
import Drafts from "scenes/Drafts";
import Error404 from "scenes/Error404";
import Login from "scenes/Login";
import Search from "scenes/Search";
import Settings from "scenes/Settings";
import Details from "scenes/Settings/Details";
import Events from "scenes/Settings/Events";
import Export from "scenes/Settings/Export";
import Groups from "scenes/Settings/Groups";
import Notifications from "scenes/Settings/Notifications";
import People from "scenes/Settings/People";
import Security from "scenes/Settings/Security";
import Shares from "scenes/Settings/Shares";
import Slack from "scenes/Settings/Slack";
import Tokens from "scenes/Settings/Tokens";
import Zapier from "scenes/Settings/Zapier";
import Starred from "scenes/Starred";
import Templates from "scenes/Templates";
import Trash from "scenes/Trash";
import Authenticated from "components/Authenticated";
import Layout from "components/Layout";
import SocketProvider from "components/SocketProvider";
import { matchDocumentSlug as slug } from "utils/routeHelpers";
const NotFound = () => <Search notFound />;
const RedirectDocument = ({ match }: { match: Match }) => (
<Redirect
to={
match.params.documentSlug ? `/doc/${match.params.documentSlug}` : "/home"
}
/>
);
export default function Routes() {
return (
<Switch>
<Route exact path="/" component={Login} />
<Route exact path="/create" component={Login} />
<Route exact path="/share/:shareId" component={KeyedDocument} />
<Authenticated>
<SocketProvider>
<Layout>
<Switch>
<Redirect from="/dashboard" to="/home" />
<Route path="/home/:tab" component={Dashboard} />
<Route path="/home" component={Dashboard} />
<Route exact path="/starred" component={Starred} />
<Route exact path="/starred/:sort" component={Starred} />
<Route exact path="/templates" component={Templates} />
<Route exact path="/templates/:sort" component={Templates} />
<Route exact path="/drafts" component={Drafts} />
<Route exact path="/archive" component={Archive} />
<Route exact path="/trash" component={Trash} />
<Route exact path="/settings" component={Settings} />
<Route exact path="/settings/details" component={Details} />
<Route exact path="/settings/security" component={Security} />
<Route exact path="/settings/people" component={People} />
<Route exact path="/settings/people/:filter" component={People} />
<Route exact path="/settings/groups" component={Groups} />
<Route exact path="/settings/shares" component={Shares} />
<Route exact path="/settings/tokens" component={Tokens} />
<Route exact path="/settings/events" component={Events} />
<Route
exact
path="/settings/notifications"
component={Notifications}
/>
<Route
exact
path="/settings/integrations/slack"
component={Slack}
/>
<Route
exact
path="/settings/integrations/zapier"
component={Zapier}
/>
<Route exact path="/settings/export" component={Export} />
<Route
exact
path="/collections/:id/new"
component={DocumentNew}
/>
<Route
exact
path="/collections/:id/:tab"
component={Collection}
/>
<Route exact path="/collections/:id" component={Collection} />
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
<Route
exact
path={`/doc/${slug}/history/:revisionId?`}
component={KeyedDocument}
/>
<Route
exact
path={`/doc/${slug}/edit`}
component={KeyedDocument}
/>
<Route path={`/doc/${slug}`} component={KeyedDocument} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:term" component={Search} />
<Route path="/404" component={Error404} />
<Route component={NotFound} />
</Switch>
</Layout>
</SocketProvider>
</Authenticated>
</Switch>
);
}
+76
View File
@@ -0,0 +1,76 @@
// @flow
import * as React from "react";
import { Switch, Route, Redirect, type Match } from "react-router-dom";
import Archive from "scenes/Archive";
import Collection from "scenes/Collection";
import Dashboard from "scenes/Dashboard";
import KeyedDocument from "scenes/Document/KeyedDocument";
import DocumentNew from "scenes/DocumentNew";
import Drafts from "scenes/Drafts";
import Error404 from "scenes/Error404";
import Search from "scenes/Search";
import Starred from "scenes/Starred";
import Templates from "scenes/Templates";
import Trash from "scenes/Trash";
import CenteredContent from "components/CenteredContent";
import Layout from "components/Layout";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import SocketProvider from "components/SocketProvider";
import { matchDocumentSlug as slug } from "utils/routeHelpers";
const SettingsRoutes = React.lazy(() => import("./settings"));
const NotFound = () => <Search notFound />;
const RedirectDocument = ({ match }: { match: Match }) => (
<Redirect
to={
match.params.documentSlug ? `/doc/${match.params.documentSlug}` : "/home"
}
/>
);
export default function AuthenticatedRoutes() {
return (
<SocketProvider>
<Layout>
<Switch>
<Redirect from="/dashboard" to="/home" />
<Route path="/home/:tab" component={Dashboard} />
<Route path="/home" component={Dashboard} />
<Route exact path="/starred" component={Starred} />
<Route exact path="/starred/:sort" component={Starred} />
<Route exact path="/templates" component={Templates} />
<Route exact path="/templates/:sort" component={Templates} />
<Route exact path="/drafts" component={Drafts} />
<Route exact path="/archive" component={Archive} />
<Route exact path="/trash" component={Trash} />
<Route exact path="/collections/:id/new" component={DocumentNew} />
<Route exact path="/collections/:id/:tab" component={Collection} />
<Route exact path="/collections/:id" component={Collection} />
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
<Route
exact
path={`/doc/${slug}/history/:revisionId?`}
component={KeyedDocument}
/>
<Route exact path={`/doc/${slug}/edit`} component={KeyedDocument} />
<Route path={`/doc/${slug}`} component={KeyedDocument} />
<Route exact path="/search" component={Search} />
<Route exact path="/search/:term" component={Search} />
<Route path="/404" component={Error404} />
<React.Suspense
fallback={
<CenteredContent>
<LoadingPlaceholder />
</CenteredContent>
}
>
<SettingsRoutes />
</React.Suspense>
<Route component={NotFound} />
</Switch>
</Layout>
</SocketProvider>
);
}
+31
View File
@@ -0,0 +1,31 @@
// @flow
import * as React from "react";
import { Switch, Route } from "react-router-dom";
import DelayedMount from "components/DelayedMount";
import FullscreenLoading from "components/FullscreenLoading";
const Authenticated = React.lazy(() => import("components/Authenticated"));
const AuthenticatedRoutes = React.lazy(() => import("./authenticated"));
const KeyedDocument = React.lazy(() => import("scenes/Document/KeyedDocument"));
const Login = React.lazy(() => import("scenes/Login"));
export default function Routes() {
return (
<React.Suspense
fallback={
<DelayedMount delay={2000}>
<FullscreenLoading />
</DelayedMount>
}
>
<Switch>
<Route exact path="/" component={Login} />
<Route exact path="/create" component={Login} />
<Route exact path="/share/:shareId" component={KeyedDocument} />
<Authenticated>
<AuthenticatedRoutes />
</Authenticated>
</Switch>
</React.Suspense>
);
}
+35
View File
@@ -0,0 +1,35 @@
// @flow
import * as React from "react";
import { Switch, Route } from "react-router-dom";
import Settings from "scenes/Settings";
import Details from "scenes/Settings/Details";
import Events from "scenes/Settings/Events";
import Export from "scenes/Settings/Export";
import Groups from "scenes/Settings/Groups";
import Notifications from "scenes/Settings/Notifications";
import People from "scenes/Settings/People";
import Security from "scenes/Settings/Security";
import Shares from "scenes/Settings/Shares";
import Slack from "scenes/Settings/Slack";
import Tokens from "scenes/Settings/Tokens";
import Zapier from "scenes/Settings/Zapier";
export default function SettingsRoutes() {
return (
<Switch>
<Route exact path="/settings" component={Settings} />
<Route exact path="/settings/details" component={Details} />
<Route exact path="/settings/security" component={Security} />
<Route exact path="/settings/people" component={People} />
<Route exact path="/settings/people/:filter" component={People} />
<Route exact path="/settings/groups" component={Groups} />
<Route exact path="/settings/shares" component={Shares} />
<Route exact path="/settings/tokens" component={Tokens} />
<Route exact path="/settings/events" component={Events} />
<Route exact path="/settings/notifications" component={Notifications} />
<Route exact path="/settings/integrations/slack" component={Slack} />
<Route exact path="/settings/integrations/zapier" component={Zapier} />
<Route exact path="/settings/export" component={Export} />
</Switch>
);
}
+19 -18
View File
@@ -5,7 +5,6 @@ import { observer, inject } from "mobx-react";
import { NewDocumentIcon, PlusIcon, PinIcon } from "outline-icons";
import * as React from "react";
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
import RichMarkdownEditor from "rich-markdown-editor";
import styled, { withTheme } from "styled-components";
import CollectionsStore from "stores/CollectionsStore";
@@ -22,6 +21,7 @@ import Button from "components/Button";
import CenteredContent from "components/CenteredContent";
import CollectionIcon from "components/CollectionIcon";
import DocumentList from "components/DocumentList";
import Editor from "components/Editor";
import Flex from "components/Flex";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
@@ -121,7 +121,7 @@ class CollectionScene extends React.Component<Props> {
return (
<Actions align="center" justify="flex-end">
{can.update && (
<React.Fragment>
<>
<Action>
<InputSearch
placeholder="Search in collection…"
@@ -141,7 +141,7 @@ class CollectionScene extends React.Component<Props> {
</Tooltip>
</Action>
<Separator />
</React.Fragment>
</>
)}
<Action>
<CollectionMenu collection={this.collection} />
@@ -165,7 +165,7 @@ class CollectionScene extends React.Component<Props> {
return (
<CenteredContent>
{collection ? (
<React.Fragment>
<>
<PageTitle title={collection.name} />
{collection.isEmpty ? (
<Centered column>
@@ -211,29 +211,30 @@ class CollectionScene extends React.Component<Props> {
</Modal>
</Centered>
) : (
<React.Fragment>
<>
<Heading>
<CollectionIcon collection={collection} size={40} expanded />{" "}
{collection.name}
</Heading>
{collection.description && (
<RichMarkdownEditor
id={collection.id}
key={collection.description}
defaultValue={collection.description}
theme={theme}
readOnly
/>
<React.Suspense fallback={<p>Loading</p>}>
<Editor
id={collection.id}
key={collection.description}
defaultValue={collection.description}
readOnly
/>
</React.Suspense>
)}
{hasPinnedDocuments && (
<React.Fragment>
<>
<Subheading>
<TinyPinIcon size={18} /> Pinned
</Subheading>
<DocumentList documents={pinnedDocuments} showPin />
</React.Fragment>
</>
)}
<Tabs>
@@ -296,18 +297,18 @@ class CollectionScene extends React.Component<Props> {
/>
</Route>
</Switch>
</React.Fragment>
</>
)}
{this.renderActions()}
</React.Fragment>
</>
) : (
<React.Fragment>
<>
<Heading>
<Mask height={35} />
</Heading>
<ListPlaceholder count={5} />
</React.Fragment>
</>
)}
</CenteredContent>
);
+1 -1
View File
@@ -49,7 +49,7 @@ class CollectionDelete extends React.Component<Props> {
<strong>{collection.name}</strong> collection is permanent and will
also delete all of the documents within it, so be extra careful.
</HelpText>
<Button type="submit" disabled={this.isDeleting} danger>
<Button type="submit" disabled={this.isDeleting} autoFocus danger>
{this.isDeleting ? "Deleting…" : "Im sure  Delete"}
</Button>
</form>
@@ -125,7 +125,7 @@ class CollectionMembers extends React.Component<Props> {
return (
<Flex column>
{collection.private ? (
<React.Fragment>
<>
<HelpText>
Choose which groups and team members have access to view and edit
documents in the private <strong>{collection.name}</strong>{" "}
@@ -146,7 +146,7 @@ class CollectionMembers extends React.Component<Props> {
Add groups
</Button>
</span>
</React.Fragment>
</>
) : (
<HelpText>
The <strong>{collection.name}</strong> collection is accessible by
@@ -195,7 +195,7 @@ class CollectionMembers extends React.Component<Props> {
</GroupsWrap>
)}
{collection.private ? (
<React.Fragment>
<>
<span>
<Button
type="button"
@@ -208,7 +208,7 @@ class CollectionMembers extends React.Component<Props> {
</span>
<Subheading>Individual Members</Subheading>
</React.Fragment>
</>
) : (
<Subheading>Members</Subheading>
)}
@@ -30,7 +30,7 @@ const MemberListItem = ({
onRemove={onRemove}
onUpdate={onUpdate}
renderActions={({ openMembersModal }) => (
<React.Fragment>
<>
<Select
label="Permissions"
options={PERMISSIONS}
@@ -51,7 +51,7 @@ const MemberListItem = ({
<DropdownMenuItem onClick={onRemove}>Remove</DropdownMenuItem>
</DropdownMenu>
</ButtonWrap>
</React.Fragment>
</>
)}
/>
);
@@ -37,17 +37,17 @@ const MemberListItem = ({
<ListItem
title={user.name}
subtitle={
<React.Fragment>
<>
{user.lastActiveAt ? (
<React.Fragment>
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
</>
) : (
"Never signed in"
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
</React.Fragment>
</>
}
image={<Avatar src={user.avatarUrl} size={40} />}
actions={
@@ -20,17 +20,17 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
title={user.name}
image={<Avatar src={user.avatarUrl} size={40} />}
subtitle={
<React.Fragment>
<>
{user.lastActiveAt ? (
<React.Fragment>
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
</>
) : (
"Never signed in"
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
</React.Fragment>
</>
}
actions={
canEdit ? (
+24 -18
View File
@@ -3,7 +3,7 @@ import invariant from "invariant";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import * as React from "react";
import type { Location, RouterHistory, Match } from "react-router-dom";
import type { RouterHistory, Match } from "react-router-dom";
import { withRouter } from "react-router-dom";
import DocumentsStore from "stores/DocumentsStore";
import PoliciesStore from "stores/PoliciesStore";
@@ -18,12 +18,13 @@ import DocumentComponent from "./Document";
import HideSidebar from "./HideSidebar";
import Loading from "./Loading";
import SocketPresence from "./SocketPresence";
import { type LocationWithState } from "types";
import { NotFoundError, OfflineError } from "utils/errors";
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
type Props = {|
match: Match,
location: Location,
location: LocationWithState,
shares: SharesStore,
documents: DocumentsStore,
policies: PoliciesStore,
@@ -63,12 +64,6 @@ class DataLoader extends React.Component<Props> {
}
}
goToDocumentCanonical = () => {
if (this.document) {
this.props.history.push(this.document.url);
}
};
get isEditing() {
return this.props.match.path === matchDocumentEdit;
}
@@ -78,7 +73,7 @@ class DataLoader extends React.Component<Props> {
return results
.filter((result) => result.document.title)
.map((result, index) => ({
.map((result) => ({
title: result.document.title,
url: result.document.url,
}));
@@ -124,17 +119,28 @@ class DataLoader extends React.Component<Props> {
const document = this.document;
if (document) {
const can = this.props.policies.abilities(document.id);
// sets the document as active in the sidebar, ideally in the future this
// will be route driven.
this.props.ui.setActiveDocument(document);
if (document.isArchived && this.isEditing) {
return this.goToDocumentCanonical();
// If we're attempting to update an archived, deleted, or otherwise
// uneditable document then forward to the canonical read url.
if (!can.update && this.isEditing) {
this.props.history.push(document.url);
return;
}
this.props.shares.fetch(document.id).catch((err) => {
if (!(err instanceof NotFoundError)) {
throw err;
}
});
// Prevents unauthorized request to load share information for the document
// when viewing a public share link
if (can.read) {
this.props.shares.fetch(document.id).catch((err) => {
if (!(err instanceof NotFoundError)) {
throw err;
}
});
}
const isMove = this.props.location.pathname.match(/move$/);
const canRedirect = !revisionId && !isMove && !shareId;
@@ -166,10 +172,10 @@ class DataLoader extends React.Component<Props> {
if (!document) {
return (
<React.Fragment>
<>
<Loading location={location} />
{this.isEditing && <HideSidebar ui={ui} />}
</React.Fragment>
</>
);
}
+10 -34
View File
@@ -6,7 +6,7 @@ import { InputIcon } from "outline-icons";
import * as React from "react";
import keydown from "react-keydown";
import { Prompt, Route, withRouter } from "react-router-dom";
import type { Location, RouterHistory, Match } from "react-router-dom";
import type { RouterHistory, Match } from "react-router-dom";
import styled, { withTheme } from "styled-components";
import breakpoint from "styled-components-breakpoint";
@@ -24,11 +24,12 @@ import Time from "components/Time";
import Container from "./Container";
import Contents from "./Contents";
import DocumentMove from "./DocumentMove";
import Editor from "./Editor";
import Header from "./Header";
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
import Loading from "./Loading";
import MarkAsViewed from "./MarkAsViewed";
import References from "./References";
import { type LocationWithState } from "types";
import { emojiToUrl } from "utils/emoji";
import {
collectionUrl,
@@ -38,7 +39,6 @@ import {
documentUrl,
} from "utils/routeHelpers";
let EditorImport;
const AUTOSAVE_DELAY = 3000;
const IS_DIRTY_DELAY = 500;
const DISCARD_CHANGES = `
@@ -53,7 +53,7 @@ Are you sure you want to discard them?
type Props = {
match: Match,
history: RouterHistory,
location: Location,
location: LocationWithState,
abilities: Object,
document: Document,
revision: Revision,
@@ -68,7 +68,6 @@ type Props = {
@observer
class DocumentScene extends React.Component<Props> {
@observable editor: ?any;
@observable editorComponent = EditorImport;
@observable isUploading: boolean = false;
@observable isSaving: boolean = false;
@observable isPublishing: boolean = false;
@@ -83,7 +82,6 @@ class DocumentScene extends React.Component<Props> {
super();
this.title = props.document.title;
this.lastRevision = props.document.revision;
this.loadEditor();
}
componentDidMount() {
@@ -196,22 +194,6 @@ class DocumentScene extends React.Component<Props> {
}
}
loadEditor = async () => {
if (this.editorComponent) return;
try {
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();
}
};
handleCloseMoveModal = () => (this.moveModalOpen = false);
handleOpenMoveModal = () => (this.moveModalOpen = true);
@@ -334,20 +316,14 @@ class DocumentScene extends React.Component<Props> {
document,
revision,
readOnly,
location,
abilities,
auth,
ui,
match,
} = this.props;
const team = auth.team;
const Editor = this.editorComponent;
const isShare = !!match.params.shareId;
if (!Editor) {
return <Loading location={location} />;
}
const value = revision ? revision.text : document.text;
const injectTemplate = document.injectTemplate;
const disableEmbeds =
@@ -375,7 +351,7 @@ class DocumentScene extends React.Component<Props> {
<Container justify="center" column auto>
{!readOnly && (
<React.Fragment>
<>
<Prompt
when={this.isDirty && !this.isUploading}
message={DISCARD_CHANGES}
@@ -384,7 +360,7 @@ class DocumentScene extends React.Component<Props> {
when={this.isUploading && !this.isDirty}
message={UPLOADING_WARNING}
/>
</React.Fragment>
</>
)}
{!isShare && (
<Header
@@ -427,12 +403,12 @@ class DocumentScene extends React.Component<Props> {
Deleted by {document.updatedBy.name}{" "}
<Time dateTime={document.deletedAt} /> ago
{document.permanentlyDeletedAt && (
<React.Fragment>
<>
<br />
This {document.noun} will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} /> unless
restored.
</React.Fragment>
</>
)}
</Notice>
)}
@@ -473,12 +449,12 @@ class DocumentScene extends React.Component<Props> {
/>
</Flex>
{readOnly && !isShare && !revision && (
<React.Fragment>
<>
<MarkAsViewed document={document} />
<ReferencesWrapper isOnlyTitle={document.isOnlyTitle}>
<References document={document} />
</ReferencesWrapper>
</React.Fragment>
</>
)}
</MaxWidth>
</Container>
+46 -36
View File
@@ -7,10 +7,12 @@ import styled from "styled-components";
import parseTitle from "shared/utils/parseTitle";
import Document from "models/Document";
import ClickablePadding from "components/ClickablePadding";
import DocumentMeta from "components/DocumentMeta";
import DocumentMetaWithViews from "components/DocumentMetaWithViews";
import Editor from "components/Editor";
import Flex from "components/Flex";
import HoverPreview from "components/HoverPreview";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {
onChangeTitle: (event: SyntheticInputEvent<>) => void,
@@ -25,23 +27,23 @@ type Props = {
@observer
class DocumentEditor extends React.Component<Props> {
@observable activeLinkEvent: ?MouseEvent;
editor: ?Editor;
editor = React.createRef<any>();
focusAtStart = () => {
if (this.editor) {
this.editor.focusAtStart();
if (this.editor.current) {
this.editor.current.focusAtStart();
}
};
focusAtEnd = () => {
if (this.editor) {
this.editor.focusAtEnd();
if (this.editor.current) {
this.editor.current.focusAtEnd();
}
};
getHeadings = () => {
if (this.editor) {
return this.editor.getHeadings();
if (this.editor.current) {
return this.editor.current.getHeadings();
}
return [];
@@ -76,35 +78,43 @@ class DocumentEditor extends React.Component<Props> {
return (
<Flex auto column>
<Title
type="text"
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder={document.placeholder}
value={!title && readOnly ? document.titleWithDefault : title}
style={startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined}
readOnly={readOnly}
autoFocus={!title}
maxLength={100}
/>
<DocumentMeta isDraft={isDraft} document={document} />
<Editor
ref={(ref) => (this.editor = ref)}
autoFocus={title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
grow
{...this.props}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (
<HoverPreview
node={this.activeLinkEvent.target}
event={this.activeLinkEvent}
onClose={this.handleLinkInactive}
<React.Suspense fallback={<LoadingPlaceholder />}>
<Title
type="text"
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder={document.placeholder}
value={!title && readOnly ? document.titleWithDefault : title}
style={
startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined
}
readOnly={readOnly}
autoFocus={!title}
maxLength={100}
/>
)}
<DocumentMetaWithViews
isDraft={isDraft}
document={document}
to={documentHistoryUrl(document)}
/>
<Editor
ref={this.editor}
autoFocus={title && !this.props.defaultValue}
placeholder="…the rest is up to you"
onHoverLink={this.handleLinkActive}
scrollTo={window.location.hash}
grow
{...this.props}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
{this.activeLinkEvent && !isShare && readOnly && (
<HoverPreview
node={this.activeLinkEvent.target}
event={this.activeLinkEvent}
onClose={this.handleLinkInactive}
/>
)}
</React.Suspense>
</Flex>
);
}
+8 -8
View File
@@ -162,7 +162,7 @@ class Header extends React.Component<Props> {
<BreadcrumbAndContents align="center" justify="flex-start">
<Breadcrumb document={document} />
{!isEditing && (
<React.Fragment>
<>
<Slash />
<Tooltip
tooltip={ui.tocVisible ? "Hide contents" : "Show contents"}
@@ -183,7 +183,7 @@ class Header extends React.Component<Props> {
small
/>
</Tooltip>
</React.Fragment>
</>
)}
</BreadcrumbAndContents>
{this.isScrolled && (
@@ -216,10 +216,10 @@ class Header extends React.Component<Props> {
<Tooltip
tooltip={
isPubliclyShared ? (
<React.Fragment>
<>
Anyone with the link <br />
can view this document
</React.Fragment>
</>
) : (
""
)
@@ -239,7 +239,7 @@ class Header extends React.Component<Props> {
</Action>
)}
{isEditing && (
<React.Fragment>
<>
<Action>
<Tooltip
tooltip="Save"
@@ -258,7 +258,7 @@ class Header extends React.Component<Props> {
</Button>
</Tooltip>
</Action>
</React.Fragment>
</>
)}
{canEdit && (
<Action>
@@ -330,7 +330,7 @@ class Header extends React.Component<Props> {
</Action>
)}
{!isEditing && (
<React.Fragment>
<>
<Separator />
<Action>
<DocumentMenu
@@ -340,7 +340,7 @@ class Header extends React.Component<Props> {
showPrint
/>
</Action>
</React.Fragment>
</>
)}
</Wrapper>
</Actions>
@@ -26,7 +26,7 @@ class KeyboardShortcutsButton extends React.Component<Props> {
render() {
return (
<React.Fragment>
<>
<Modal
isOpen={this.keyboardShortcutsOpen}
onRequestClose={this.handleCloseKeyboardShortcuts}
@@ -44,7 +44,7 @@ class KeyboardShortcutsButton extends React.Component<Props> {
<KeyboardIcon />
</Button>
</Tooltip>
</React.Fragment>
</>
);
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
// @flow
import * as React from "react";
import type { Location } from "react-router-dom";
import CenteredContent from "components/CenteredContent";
import LoadingPlaceholder from "components/LoadingPlaceholder";
import PageTitle from "components/PageTitle";
import Container from "./Container";
import type { LocationWithState } from "types";
type Props = {|
location: Location,
location: LocationWithState,
|};
export default function Loading({ location }: Props) {
@@ -4,7 +4,7 @@ import * as React from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import Document from "models/Document";
import PublishingInfo from "components/PublishingInfo";
import DocumentMeta from "components/DocumentMeta";
import type { NavigationNode } from "types";
type Props = {
@@ -60,7 +60,7 @@ class ReferenceListItem extends React.Component<Props> {
>
<Title>{document.title}</Title>
{document.updatedBy && (
<PublishingInfo document={document} showCollection={showCollection} />
<DocumentMeta document={document} showCollection={showCollection} />
)}
</DocumentLink>
);
@@ -41,7 +41,7 @@ export default class SocketPresence extends React.Component<Props> {
}
setupOnce = () => {
if (this.context && !this.previousContext) {
if (this.context && this.context !== this.previousContext) {
this.previousContext = this.context;
if (this.context.authenticated) {
+2 -2
View File
@@ -80,7 +80,7 @@ class DocumentShare extends React.Component<Props> {
.
</HelpText>
{canPublish && (
<React.Fragment>
<>
<Switch
id="published"
label="Publish to internet"
@@ -96,7 +96,7 @@ class DocumentShare extends React.Component<Props> {
: "Only team members with access can view this document"}
</PrivacyText>
</Privacy>
</React.Fragment>
</>
)}
<br />
<Input
+2 -2
View File
@@ -62,7 +62,7 @@ class GroupMembers extends React.Component<Props> {
return (
<Flex column>
{can.update ? (
<React.Fragment>
<>
<HelpText>
Add and remove team members in the <strong>{group.name}</strong>{" "}
group. Adding people to the group will give them access to any
@@ -78,7 +78,7 @@ class GroupMembers extends React.Component<Props> {
Add people
</Button>
</span>
</React.Fragment>
</>
) : (
<HelpText>
Listing team members in the <strong>{group.name}</strong> group.
@@ -27,17 +27,17 @@ const GroupMemberListItem = ({
<ListItem
title={user.name}
subtitle={
<React.Fragment>
<>
{user.lastActiveAt ? (
<React.Fragment>
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
</>
) : (
"Never signed in"
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
</React.Fragment>
</>
}
image={<Avatar src={user.avatarUrl} size={40} />}
actions={
@@ -20,17 +20,17 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
title={user.name}
image={<Avatar src={user.avatarUrl} size={32} />}
subtitle={
<React.Fragment>
<>
{user.lastActiveAt ? (
<React.Fragment>
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
</>
) : (
"Never signed in"
)}
{!user.lastActiveAt && <Badge>Invited</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
</React.Fragment>
</>
}
actions={
canEdit ? (
+2 -2
View File
@@ -51,7 +51,7 @@ class GroupNew extends React.Component<Props> {
render() {
return (
<React.Fragment>
<>
<form onSubmit={this.handleSubmit}>
<HelpText>
Groups are for organizing your team. They work best when centered
@@ -82,7 +82,7 @@ class GroupNew extends React.Component<Props> {
>
<GroupMembers group={this.group} />
</Modal>
</React.Fragment>
</>
);
}
}
+2 -2
View File
@@ -111,10 +111,10 @@ class Invite extends React.Component<Props> {
Invite team members to join your knowledge base. They will need to
sign in with {team.signinMethods}.{" "}
{can.update && (
<React.Fragment>
<>
As an admin you can also{" "}
<Link to="/settings/security">enable email sign-in</Link>.
</React.Fragment>
</>
)}
</HelpText>
)}
+2 -2
View File
@@ -8,7 +8,7 @@ type Props = {
export default function Notices({ notice }: Props) {
return (
<React.Fragment>
<>
{notice === "google-hd" && (
<NoticeAlert>
Sorry, Google sign in cannot be used with a personal email. Please try
@@ -51,6 +51,6 @@ export default function Notices({ notice }: Props) {
please contact a team admin.
</NoticeAlert>
)}
</React.Fragment>
</>
);
}
+2 -2
View File
@@ -72,7 +72,7 @@ class Service extends React.Component<Props, State> {
onSubmit={this.handleSubmitEmail}
>
{this.state.showEmailSignin ? (
<React.Fragment>
<>
<InputLarge
type="email"
name="email"
@@ -87,7 +87,7 @@ class Service extends React.Component<Props, State> {
<ButtonLarge type="submit" disabled={this.state.isSubmitting}>
Sign In
</ButtonLarge>
</React.Fragment>
</>
) : (
<ButtonLarge type="submit" icon={<EmailIcon />} fullwidth>
Continue with Email
+5 -5
View File
@@ -82,7 +82,7 @@ class Login extends React.Component<Props, State> {
<PageTitle title="Check your email" />
<CheckEmailIcon size={38} color="currentColor" />
<Heading>Check your email</Heading>
<Heading centered>Check your email</Heading>
<Note>
A magic sign-in link has been sent to the email{" "}
<em>{this.state.emailLinkSentTo}</em>, no password needed.
@@ -110,9 +110,9 @@ class Login extends React.Component<Props, State> {
</Logo>
{isCreate ? (
<Heading>Create an account</Heading>
<Heading centered>Create an account</Heading>
) : (
<Heading>Login to {config.name || "Outline"}</Heading>
<Heading centered>Login to {config.name || "Outline"}</Heading>
)}
<Notices notice={getQueryVariable("notice")} />
@@ -125,12 +125,12 @@ class Login extends React.Component<Props, State> {
{...defaultService}
/>
{hasMultipleServices && (
<React.Fragment>
<>
<Note>
You signed in with {defaultService.name} last time.
</Note>
<Or />
</React.Fragment>
</>
)}
</React.Fragment>
)}
+12 -3
View File
@@ -9,9 +9,10 @@ import * as React from "react";
import ReactDOM from "react-dom";
import keydown from "react-keydown";
import { withRouter, Link } from "react-router-dom";
import type { Location, RouterHistory, Match } from "react-router-dom";
import type { RouterHistory, Match } from "react-router-dom";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DocumentsStore from "stores/DocumentsStore";
@@ -32,13 +33,14 @@ import SearchField from "./components/SearchField";
import StatusFilter from "./components/StatusFilter";
import UserFilter from "./components/UserFilter";
import NewDocumentMenu from "menus/NewDocumentMenu";
import { type LocationWithState } from "types";
import { meta } from "utils/keyboard";
import { newDocumentUrl, searchUrl } from "utils/routeHelpers";
type Props = {
history: RouterHistory,
match: Match,
location: Location,
location: LocationWithState,
documents: DocumentsStore,
users: UsersStore,
notFound: ?boolean,
@@ -46,7 +48,7 @@ type Props = {
@observer
class Search extends React.Component<Props> {
firstDocument: ?DocumentPreview;
firstDocument: ?React.Component<typeof DocumentPreview>;
@observable
query: string = decodeURIComponent(this.props.match.params.term || "");
@@ -222,6 +224,7 @@ class Search extends React.Component<Props> {
};
setFirstDocumentRef = (ref) => {
// $FlowFixMe
this.firstDocument = ref;
};
@@ -385,6 +388,12 @@ const Filters = styled(Flex)`
margin-bottom: 12px;
opacity: 0.85;
transition: opacity 100ms ease-in-out;
overflow-y: auto;
padding: 8px 0;
${breakpoint("tablet")`
padding: 0;
`};
&:hover {
opacity: 1;
+2 -2
View File
@@ -117,7 +117,7 @@ class Details extends React.Component<Props> {
short
/>
{env.SUBDOMAINS_ENABLED && (
<React.Fragment>
<>
<Input
label="Subdomain"
name="subdomain"
@@ -134,7 +134,7 @@ class Details extends React.Component<Props> {
<strong>{this.subdomain}.getoutline.com</strong>
</HelpText>
)}
</React.Fragment>
</>
)}
<Button type="submit" disabled={isSaving || !this.isValid}>
{isSaving ? "Saving…" : "Save"}
+2 -2
View File
@@ -84,14 +84,14 @@ class Events extends React.Component<Props> {
{showLoading ? (
<ListPlaceholder count={5} />
) : (
<React.Fragment>
<>
{events.orderedData.map((event) => (
<EventListItem event={event} />
))}
{this.allowLoadMore && (
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
)}
</React.Fragment>
</>
)}
</List>
</CenteredContent>
+2 -2
View File
@@ -104,12 +104,12 @@ class People extends React.Component<Props> {
</Tab>
{can.invite && (
<React.Fragment>
<>
<Separator />
<Tab to="/settings/people/invited" exact>
Invited
</Tab>
</React.Fragment>
</>
)}
</Tabs>
<PaginatedList
+49 -51
View File
@@ -16,121 +16,119 @@ const description = (event) => {
switch (event.name) {
case "api_keys.create":
return (
<React.Fragment>
<>
Created the API token <strong>{event.data.name}</strong>
</React.Fragment>
</>
);
case "api_keys.delete":
return (
<React.Fragment>
<>
Revoked the API token <strong>{event.data.name}</strong>
</React.Fragment>
</>
);
case "teams.create":
return "Created the team";
case "shares.create":
case "shares.revoke":
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} a{" "}
<Link to={`/share/${event.modelId || ""}`}>share link</Link> to the{" "}
<Link to={`/doc/${event.documentId}`}>{event.data.name}</Link>{" "}
document
</React.Fragment>
</>
);
case "shares.update":
return (
<React.Fragment>
<>
{event.data.published ? (
<React.Fragment>
<>
Published a document{" "}
<Link to={`/share/${event.modelId || ""}`}>share link</Link>
</React.Fragment>
</>
) : (
<React.Fragment>
<>
Unpublished a document{" "}
<Link to={`/share/${event.modelId || ""}`}>share link</Link>
</React.Fragment>
</>
)}
</React.Fragment>
</>
);
case "users.create":
return (
<React.Fragment>{event.data.name} created an account</React.Fragment>
);
return <>{event.data.name} created an account</>;
case "users.invite":
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} {event.data.name} (
<a href={`mailto:${event.data.email || ""}`}>
{event.data.email || ""}
</a>
)
</React.Fragment>
</>
);
case "users.suspend":
return (
<React.Fragment>
<>
Suspended <strong>{event.data.name}s</strong> account
</React.Fragment>
</>
);
case "users.activate":
return (
<React.Fragment>
<>
Unsuspended <strong>{event.data.name}s</strong> account
</React.Fragment>
</>
);
case "users.promote":
return (
<React.Fragment>
<>
Made <strong>{event.data.name}</strong> an admin
</React.Fragment>
</>
);
case "users.demote":
return (
<React.Fragment>
<>
Made <strong>{event.data.name}</strong> a member
</React.Fragment>
</>
);
case "users.delete":
return "Deleted their account";
case "groups.create":
return (
<React.Fragment>
<>
Created the group <strong>{event.data.name}</strong>
</React.Fragment>
</>
);
case "groups.update":
return (
<React.Fragment>
<>
Update the group <strong>{event.data.name}</strong>
</React.Fragment>
</>
);
case "groups.delete":
return (
<React.Fragment>
<>
Deleted the group <strong>{event.data.name}</strong>
</React.Fragment>
</>
);
case "collections.add_user":
case "collections.add_group":
return (
<React.Fragment>
<>
Granted <strong>{event.data.name}</strong> access to a{" "}
<Link to={`/collections/${event.collectionId || ""}`}>
collection
</Link>
</React.Fragment>
</>
);
case "collections.remove_user":
case "collections.remove_group":
return (
<React.Fragment>
<>
Revoked <strong>{event.data.name}</strong> access to a{" "}
<Link to={`/collections/${event.collectionId || ""}`}>
collection
</Link>
</React.Fragment>
</>
);
default:
}
@@ -138,60 +136,60 @@ const description = (event) => {
if (event.documentId) {
if (event.name === "documents.delete") {
return (
<React.Fragment>
<>
Deleted the <strong>{event.data.title}</strong> document
</React.Fragment>
</>
);
}
if (event.name === "documents.create") {
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} the{" "}
<Link to={`/doc/${event.documentId}`}>
{event.data.title || "Untitled"}
</Link>{" "}
document{" "}
{event.data.templateId && (
<React.Fragment>
<>
from a <Link to={`/doc/${event.data.templateId}`}>template</Link>
</React.Fragment>
</>
)}
</React.Fragment>
</>
);
}
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} the{" "}
<Link to={`/doc/${event.documentId}`}>
{event.data.title || "Untitled"}
</Link>{" "}
document
</React.Fragment>
</>
);
}
if (event.collectionId) {
if (event.name === "collections.delete") {
return (
<React.Fragment>
<>
Deleted the <strong>{event.data.name}</strong> collection
</React.Fragment>
</>
);
}
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} the{" "}
<Link to={`/collections/${event.collectionId || ""}`}>
{event.data.name}
</Link>{" "}
collection
</React.Fragment>
</>
);
}
if (event.userId) {
return (
<React.Fragment>
<>
{capitalize(event.verbPastTense)} the user {event.data.name}
</React.Fragment>
</>
);
}
return "";
@@ -204,10 +202,10 @@ const EventListItem = ({ event }: Props) => {
title={event.actor.name}
image={<Avatar src={event.actor.avatarUrl} size={32} />}
subtitle={
<React.Fragment>
<>
{description(event)} <Time dateTime={event.createdAt} /> ago &middot;{" "}
<strong>{event.name}</strong>
</React.Fragment>
</>
}
actions={
event.actorIpAddress ? (
@@ -16,7 +16,6 @@ const NotificationListItem = ({
setting,
title,
event,
enabled,
onChange,
disabled,
description,
@@ -15,10 +15,10 @@ const ShareListItem = ({ share }: Props) => {
key={share.id}
title={share.documentTitle}
subtitle={
<React.Fragment>
<>
Shared <Time dateTime={share.createdAt} /> ago by{" "}
{share.createdBy.name}
</React.Fragment>
</>
}
actions={<ShareMenu share={share} />}
/>
@@ -6,7 +6,7 @@ import ListItem from "components/List/Item";
type Props = {
token: ApiKey,
onDelete: (tokenId: string) => void,
onDelete: (tokenId: string) => Promise<void>,
};
const TokenListItem = ({ token, onDelete }: Props) => {
@@ -35,7 +35,7 @@ class UserListItem extends React.Component<Props> {
<ListItem
title={<Title onClick={this.handleOpenProfile}>{user.name}</Title>}
image={
<React.Fragment>
<>
<Avatar
src={user.avatarUrl}
size={40}
@@ -46,21 +46,21 @@ class UserListItem extends React.Component<Props> {
isOpen={this.profileOpen}
onRequestClose={this.handleCloseProfile}
/>
</React.Fragment>
</>
}
subtitle={
<React.Fragment>
<>
{user.email ? `${user.email} · ` : undefined}
{user.lastActiveAt ? (
<React.Fragment>
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</React.Fragment>
</>
) : (
"Invited"
)}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{user.isSuspended && <Badge>Suspended</Badge>}
</React.Fragment>
</>
}
actions={showMenu ? <UserMenu user={user} /> : undefined}
/>
+12 -24
View File
@@ -4,7 +4,7 @@ import { observable, action, autorun, computed } from "mobx";
import { v4 } from "uuid";
import Collection from "models/Collection";
import Document from "models/Document";
import type { Toast } from "../types";
import type { Toast } from "types";
const UI_STORE = "UI_STORE";
@@ -14,8 +14,6 @@ class UiStore {
// systemTheme represents the system UI theme (Settings -> General in macOS)
@observable systemTheme: "light" | "dark";
@observable activeModalName: ?string;
@observable activeModalProps: ?Object;
@observable activeDocumentId: ?string;
@observable activeCollectionId: ?string;
@observable progressBarVisible: boolean = false;
@@ -34,16 +32,18 @@ class UiStore {
}
// system theme listeners
const colorSchemeQueryList = window.matchMedia(
"(prefers-color-scheme: dark)"
);
if (window.matchMedia) {
const colorSchemeQueryList = window.matchMedia(
"(prefers-color-scheme: dark)"
);
const setSystemTheme = (event) => {
this.systemTheme = event.matches ? "dark" : "light";
};
setSystemTheme(colorSchemeQueryList);
if (colorSchemeQueryList.addListener) {
colorSchemeQueryList.addListener(setSystemTheme);
const setSystemTheme = (event) => {
this.systemTheme = event.matches ? "dark" : "light";
};
setSystemTheme(colorSchemeQueryList);
if (colorSchemeQueryList.addListener) {
colorSchemeQueryList.addListener(setSystemTheme);
}
}
// persisted keys
@@ -68,18 +68,6 @@ class UiStore {
}
};
@action
setActiveModal = (name: string, props: ?Object): void => {
this.activeModalName = name;
this.activeModalProps = props;
};
@action
clearActiveModal = (): void => {
this.activeModalName = undefined;
this.activeModalProps = undefined;
};
@action
setActiveDocument = (document: Document): void => {
this.activeDocumentId = document.id;
+7
View File
@@ -1,6 +1,13 @@
// @flow
import { type Location } from "react-router-dom";
import Document from "models/Document";
export type LocationWithState = Location & {
state: {
[key: string]: string,
},
};
export type Toast = {
id: string,
createdAt: string,
+1 -2
View File
@@ -1,7 +1,6 @@
// @flow
import invariant from "invariant";
import { map, trim } from "lodash";
import pkg from "rich-markdown-editor/package.json";
import stores from "stores";
import download from "./download";
import {
@@ -57,7 +56,7 @@ class ApiClient {
Accept: "application/json",
"Content-Type": "application/json",
"cache-control": "no-cache",
"x-editor-version": pkg.version,
"x-editor-version": EDITOR_VERSION,
pragma: "no-cache",
});
if (stores.auth.authenticated) {
+25
View File
@@ -0,0 +1,25 @@
// @flow
let hidden = "hidden";
let visibilityChange = "visibilitychange";
if ("hidden" in document) {
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if ("mozHidden" in document) {
// Firefox up to v17
hidden = "mozHidden";
visibilityChange = "mozvisibilitychange";
} else if ("webkitHidden" in document) {
// Chrome up to v32, Android up to v4.4, Blackberry up to v10
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
export function getVisibilityListener(): string {
return visibilityChange;
}
export function getPageVisible(): boolean {
// $FlowFixMe
return !document[hidden];
}
+2
View File
@@ -4,3 +4,5 @@ declare var process: {
[string]: string,
},
};
declare var EDITOR_VERSION: string;
-119
View File
@@ -1,119 +0,0 @@
// flow-typed signature: d828559e8abc3863ee0f8ef88f5b646e
// flow-typed version: <<STUB>>/identity-obj-proxy_v^3.0.0/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'identity-obj-proxy'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'identity-obj-proxy' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'identity-obj-proxy/src/__tests__/import-es6-export-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/import-es6-import-export-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/import-es6-import-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/import-vanilla-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/index-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-export-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-import-export-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-import-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/__tests__/require-vanilla-test' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6Export' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6Import' {
declare module.exports: any;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6ImportExport' {
declare module.exports: any;
}
// Filename aliases
declare module 'identity-obj-proxy/src/__tests__/import-es6-export-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/import-es6-export-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/import-es6-import-export-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/import-es6-import-export-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/import-es6-import-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/import-es6-import-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/import-vanilla-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/import-vanilla-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/index-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/index-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-export-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/require-es6-export-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-import-export-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/require-es6-import-export-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/require-es6-import-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/require-es6-import-test'>;
}
declare module 'identity-obj-proxy/src/__tests__/require-vanilla-test.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/__tests__/require-vanilla-test'>;
}
declare module 'identity-obj-proxy/src/index' {
declare module.exports: $Exports<'identity-obj-proxy/src'>;
}
declare module 'identity-obj-proxy/src/index.js' {
declare module.exports: $Exports<'identity-obj-proxy/src'>;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6Export.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/test-redirections/idObjES6Export'>;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6Import.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/test-redirections/idObjES6Import'>;
}
declare module 'identity-obj-proxy/src/test-redirections/idObjES6ImportExport.js' {
declare module.exports: $Exports<'identity-obj-proxy/src/test-redirections/idObjES6ImportExport'>;
}
-83
View File
@@ -1,83 +0,0 @@
// flow-typed signature: 90824cfc39ff764d3f06f9f71bdb6ef1
// flow-typed version: <<STUB>>/uglifyjs-webpack-plugin_v1.2.5/flow_v0.104.0
/**
* This is an autogenerated libdef stub for:
*
* 'uglifyjs-webpack-plugin'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'uglifyjs-webpack-plugin' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'uglifyjs-webpack-plugin/dist/cjs' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/minify' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/versions' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/worker' {
declare module.exports: any;
}
declare module 'uglifyjs-webpack-plugin/dist/utils' {
declare module.exports: any;
}
// Filename aliases
declare module 'uglifyjs-webpack-plugin/dist/cjs.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/cjs'>;
}
declare module 'uglifyjs-webpack-plugin/dist/index' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist'>;
}
declare module 'uglifyjs-webpack-plugin/dist/index.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist'>;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/index' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/uglify'>;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/index.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/uglify'>;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/minify.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/uglify/minify'>;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/versions.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/uglify/versions'>;
}
declare module 'uglifyjs-webpack-plugin/dist/uglify/worker.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/uglify/worker'>;
}
declare module 'uglifyjs-webpack-plugin/dist/utils/index' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/utils'>;
}
declare module 'uglifyjs-webpack-plugin/dist/utils/index.js' {
declare module.exports: $Exports<'uglifyjs-webpack-plugin/dist/utils'>;
}
+22 -21
View File
@@ -56,7 +56,14 @@
},
"browserslist": "> 0.25%, not dead",
"dependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-destructuring": "^7.10.4",
"@babel/plugin-transform-regenerator": "^7.10.4",
"@babel/polyfill": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.10.5",
"@rehooks/window-scroll-position": "^1.0.1",
"@sentry/node": "^5.12.2",
@@ -64,6 +71,9 @@
"@tommoor/remove-markdown": "0.3.1",
"autotrack": "^2.4.1",
"aws-sdk": "^2.135.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-styled-components": "^1.11.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"boundless-arrow-key-navigation": "^1.0.4",
"boundless-popover": "^1.0.4",
"bull": "^3.5.2",
@@ -80,7 +90,6 @@
"flow-typed": "^2.6.2",
"fs-extra": "^4.0.2",
"google-auth-library": "^5.5.1",
"html-webpack-plugin": "3.2.0",
"http-errors": "1.4.0",
"immutable": "^3.8.2",
"imports-loader": "0.6.5",
@@ -129,7 +138,7 @@
"react-portal": "^4.0.0",
"react-router-dom": "^5.1.2",
"react-waypoint": "^9.0.2",
"rich-markdown-editor": "^10.5.0",
"rich-markdown-editor": "^10.6.1",
"semver": "^7.3.2",
"sequelize": "^5.21.1",
"sequelize-cli": "^5.5.0",
@@ -146,27 +155,14 @@
"styled-normalize": "^8.0.4",
"tiny-cookie": "^2.3.1",
"tmp": "0.0.33",
"uglifyjs-webpack-plugin": "1.2.5",
"url-loader": "^0.6.2",
"uuid": "2.0.2",
"validator": "5.2.0",
"webpack": "3.10.0",
"webpack-manifest-plugin": "^1.3.2"
"validator": "5.2.0"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-destructuring": "^7.10.4",
"@babel/plugin-transform-regenerator": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@relative-ci/agent": "^1.3.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.2.2",
"babel-loader": "^8.1.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-styled-components": "^1.11.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"eslint": "^7.6.0",
"eslint-config-react-app": "3.0.6",
"eslint-plugin-flowtype": "^5.2.0",
@@ -176,18 +172,23 @@
"eslint-plugin-react": "^7.20.0",
"fetch-test-server": "^1.1.0",
"flow-bin": "^0.104.0",
"identity-obj-proxy": "^3.0.0",
"html-webpack-plugin": "3.2.0",
"jest-cli": "^26.0.0",
"koa-webpack-dev-middleware": "^1.4.5",
"koa-webpack-hot-middleware": "^1.0.3",
"mobx-react-devtools": "^6.0.3",
"nodemon": "^1.19.4",
"prettier": "^2.0.5",
"rimraf": "^2.5.4"
"rimraf": "^2.5.4",
"terser-webpack-plugin": "^4.1.0",
"url-loader": "^0.6.2",
"webpack": "4.44.1",
"webpack-cli": "^3.3.12",
"webpack-manifest-plugin": "^2.2.0"
},
"resolutions": {
"dot-prop": "^5.2.0",
"js-yaml": "^3.13.1"
},
"version": "0.45.0"
}
"version": "0.46.0"
}
+1 -2
View File
@@ -30,7 +30,7 @@ const { authorize } = policy;
const router = new Router();
router.post("collections.create", auth(), async (ctx) => {
const { name, color, description, icon, type } = ctx.body;
const { name, color, description, icon } = ctx.body;
const isPrivate = ctx.body.private;
ctx.assertPresent(name, "name is required");
@@ -46,7 +46,6 @@ router.post("collections.create", auth(), async (ctx) => {
description,
icon,
color,
type: type || "atlas",
teamId: user.teamId,
creatorId: user.id,
private: isPrivate,
-1
View File
@@ -1062,7 +1062,6 @@ describe("#collections.delete", () => {
urlId: "blah",
teamId: user.teamId,
creatorId: user.id,
type: "atlas",
});
const res = await server.post("/api/collections.delete", {

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