diff --git a/README.md b/README.md
index 33481fab2..da2636cc2 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
An open, extensible, wiki for your team built using React and Node.js.
Try out Outline using our hosted version at www.getoutline.com.
-
+
diff --git a/app.json b/app.json
index 5aa7e9909..4af94bb08 100644
--- a/app.json
+++ b/app.json
@@ -30,6 +30,10 @@
"postdeploy": "yarn sequelize db:migrate"
},
"env": {
+ "NODE_ENV": {
+ "value": "production",
+ "required": true
+ },
"SECRET_KEY": {
"description": "A secret key",
"generator": "secret",
@@ -144,4 +148,4 @@
"required": false
}
}
-}
\ No newline at end of file
+}
diff --git a/app/components/Arrow.js b/app/components/Arrow.js
new file mode 100644
index 000000000..2b6ade160
--- /dev/null
+++ b/app/components/Arrow.js
@@ -0,0 +1,23 @@
+// @flow
+import * as React from "react";
+
+export default function Arrow() {
+ return (
+
+ );
+}
diff --git a/app/components/CenteredContent.js b/app/components/CenteredContent.js
index 0a868cc56..b25801640 100644
--- a/app/components/CenteredContent.js
+++ b/app/components/CenteredContent.js
@@ -3,17 +3,18 @@ import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
-type Props = {
+type Props = {|
children?: React.Node,
-};
+ withStickyHeader?: boolean,
+|};
const Container = styled.div`
width: 100%;
max-width: 100vw;
- padding: 60px 20px;
+ padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
${breakpoint("tablet")`
- padding: 60px;
+ padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
`};
`;
diff --git a/app/components/Collaborators.js b/app/components/Collaborators.js
index 2292f45fd..b5f65ba62 100644
--- a/app/components/Collaborators.js
+++ b/app/components/Collaborators.js
@@ -2,8 +2,9 @@
import { sortBy, keyBy } from "lodash";
import { observer, inject } from "mobx-react";
import * as React from "react";
+import styled from "styled-components";
+import breakpoint from "styled-components-breakpoint";
import { MAX_AVATAR_DISPLAY } from "shared/constants";
-
import DocumentPresenceStore from "stores/DocumentPresenceStore";
import ViewsStore from "stores/ViewsStore";
import Document from "models/Document";
@@ -51,7 +52,7 @@ class Collaborators extends React.Component {
const overflow = documentViews.length - mostRecentViewers.length;
return (
- v.user)}
overflow={overflow}
renderAvatar={(user) => {
@@ -75,4 +76,10 @@ class Collaborators extends React.Component {
}
}
+const FacepileHiddenOnMobile = styled(Facepile)`
+ ${breakpoint("mobile", "tablet")`
+ display: none;
+ `};
+`;
+
export default inject("views", "presence")(Collaborators);
diff --git a/app/components/CollectionDescription.js b/app/components/CollectionDescription.js
new file mode 100644
index 000000000..1e13b461a
--- /dev/null
+++ b/app/components/CollectionDescription.js
@@ -0,0 +1,212 @@
+// @flow
+import { observer } from "mobx-react";
+import { transparentize } from "polished";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+import Collection from "models/Collection";
+import Arrow from "components/Arrow";
+import ButtonLink from "components/ButtonLink";
+import Editor from "components/Editor";
+import LoadingIndicator from "components/LoadingIndicator";
+import NudeButton from "components/NudeButton";
+import useDebouncedCallback from "hooks/useDebouncedCallback";
+import useStores from "hooks/useStores";
+
+type Props = {|
+ collection: Collection,
+|};
+
+function CollectionDescription({ collection }: Props) {
+ const { collections, ui, policies } = useStores();
+ const { t } = useTranslation();
+ const [isExpanded, setExpanded] = React.useState(false);
+ const [isEditing, setEditing] = React.useState(false);
+ const [isDirty, setDirty] = React.useState(false);
+ const can = policies.abilities(collection.id);
+
+ const handleStartEditing = React.useCallback(() => {
+ setEditing(true);
+ }, []);
+
+ const handleStopEditing = React.useCallback(() => {
+ setEditing(false);
+ }, []);
+
+ const handleClickDisclosure = React.useCallback(
+ (event) => {
+ event.preventDefault();
+
+ if (isExpanded && document.activeElement) {
+ document.activeElement.blur();
+ }
+
+ setExpanded(!isExpanded);
+ },
+ [isExpanded]
+ );
+
+ const handleSave = useDebouncedCallback(async (getValue) => {
+ try {
+ await collection.save({
+ description: getValue(),
+ });
+ setDirty(false);
+ } catch (err) {
+ ui.showToast(
+ t("Sorry, an error occurred saving the collection", {
+ type: "error",
+ })
+ );
+ throw err;
+ }
+ }, 1000);
+
+ const handleChange = React.useCallback(
+ (getValue) => {
+ setDirty(true);
+ handleSave(getValue);
+ },
+ [handleSave]
+ );
+
+ React.useEffect(() => {
+ setEditing(false);
+ }, [collection.id]);
+
+ const placeholder = `${t("Add a description")}…`;
+ const key = isEditing || isDirty ? "draft" : collection.updatedAt;
+
+ return (
+
+
+
+ {collections.isSaving && }
+ {collection.hasDescription || isEditing || isDirty ? (
+ Loading…}>
+
+
+ ) : (
+ can.update && {placeholder}
+ )}
+
+
+ {!isEditing && (
+
+
+
+ )}
+
+ );
+}
+
+const Disclosure = styled(NudeButton)`
+ opacity: 0;
+ color: ${(props) => props.theme.divider};
+ position: absolute;
+ top: calc(25vh - 50px);
+ left: 50%;
+ z-index: 1;
+ transform: rotate(-90deg) translateX(-50%);
+ transition: opacity 100ms ease-in-out;
+
+ &:focus,
+ &:hover {
+ opacity: 1;
+ }
+
+ &:active {
+ color: ${(props) => props.theme.sidebarText};
+ }
+`;
+
+const Placeholder = styled(ButtonLink)`
+ color: ${(props) => props.theme.placeholder};
+ cursor: text;
+ min-height: 27px;
+`;
+
+const MaxHeight = styled.div`
+ position: relative;
+ max-height: 25vh;
+ overflow: hidden;
+ margin: -8px;
+ padding: 8px;
+
+ &[data-editing="true"],
+ &[data-expanded="true"] {
+ max-height: initial;
+ overflow: initial;
+
+ ${Disclosure} {
+ top: initial;
+ bottom: 0;
+ transform: rotate(90deg) translateX(-50%);
+ }
+ }
+
+ &:hover ${Disclosure} {
+ opacity: 1;
+ }
+`;
+
+const Input = styled.div`
+ margin: -8px;
+ padding: 8px;
+ border-radius: 8px;
+ transition: ${(props) => props.theme.backgroundTransition};
+
+ &:after {
+ content: "";
+ position: absolute;
+ top: calc(25vh - 50px);
+ left: 0;
+ right: 0;
+ height: 50px;
+ pointer-events: none;
+ background: linear-gradient(
+ 180deg,
+ ${(props) => transparentize(1, props.theme.background)} 0%,
+ ${(props) => props.theme.background} 100%
+ );
+ }
+
+ &[data-editing="true"],
+ &[data-expanded="true"] {
+ &:after {
+ background: transparent;
+ }
+ }
+
+ &[data-editing="true"] {
+ background: ${(props) => props.theme.secondaryBackground};
+ }
+
+ .block-menu-trigger,
+ .heading-anchor {
+ display: none !important;
+ }
+`;
+
+export default observer(CollectionDescription);
diff --git a/app/components/DocumentListItem.js b/app/components/DocumentListItem.js
index 78fe61d54..1a52d4f6c 100644
--- a/app/components/DocumentListItem.js
+++ b/app/components/DocumentListItem.js
@@ -163,8 +163,11 @@ const DocumentLink = styled(Link)`
padding: 6px 8px;
border-radius: 8px;
max-height: 50vh;
- min-width: 100%;
- max-width: calc(100vw - 40px);
+ width: calc(100vw - 8px);
+
+ ${breakpoint("tablet")`
+ width: auto;
+ `};
${Actions} {
opacity: 0;
diff --git a/app/components/Editor.js b/app/components/Editor.js
index 103d87777..37d01c5f8 100644
--- a/app/components/Editor.js
+++ b/app/components/Editor.js
@@ -27,13 +27,16 @@ export type Props = {|
autoFocus?: boolean,
template?: boolean,
placeholder?: string,
+ maxLength?: number,
scrollTo?: string,
+ handleDOMEvents?: Object,
readOnlyWriteCheckboxes?: boolean,
onBlur?: (event: SyntheticEvent<>) => any,
onFocus?: (event: SyntheticEvent<>) => any,
onPublish?: (event: SyntheticEvent<>) => any,
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
onCancel?: () => any,
+ onDoubleClick?: () => any,
onChange?: (getValue: () => string) => any,
onSearchLink?: (title: string) => any,
onHoverLink?: (event: MouseEvent) => any,
@@ -177,7 +180,7 @@ const StyledEditor = styled(RichMarkdownEditor)`
justify-content: start;
> div {
- transition: ${(props) => props.theme.backgroundTransition};
+ background: transparent;
}
& * {
diff --git a/app/components/Header.js b/app/components/Header.js
new file mode 100644
index 000000000..9a0e67908
--- /dev/null
+++ b/app/components/Header.js
@@ -0,0 +1,104 @@
+// @flow
+import { throttle } from "lodash";
+import { observer } from "mobx-react";
+import { transparentize } from "polished";
+import * as React from "react";
+import styled from "styled-components";
+import breakpoint from "styled-components-breakpoint";
+import Fade from "components/Fade";
+import Flex from "components/Flex";
+
+type Props = {|
+ breadcrumb?: React.Node,
+ title: React.Node,
+ actions?: React.Node,
+|};
+
+function Header({ breadcrumb, title, actions }: Props) {
+ const [isScrolled, setScrolled] = React.useState(false);
+
+ const handleScroll = React.useCallback(
+ throttle(() => setScrolled(window.scrollY > 75), 50),
+ []
+ );
+
+ React.useEffect(() => {
+ window.addEventListener("scroll", handleScroll);
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [handleScroll]);
+
+ const handleClickTitle = React.useCallback(() => {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ });
+ }, []);
+
+ return (
+
+ {breadcrumb}
+ {isScrolled ? (
+
+
+ {title}
+
+
+ ) : (
+
+ )}
+ {actions && {actions}}
+
+ );
+}
+
+const Wrapper = styled(Flex)`
+ position: sticky;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 2;
+ background: ${(props) => transparentize(0.2, props.theme.background)};
+ padding: 12px;
+ transition: all 100ms ease-out;
+ transform: translate3d(0, 0, 0);
+ backdrop-filter: blur(20px);
+
+ @media print {
+ display: none;
+ }
+
+ ${breakpoint("tablet")`
+ padding: ${(props) => (props.isCompact ? "12px" : `24px 24px 0`)};
+ `};
+`;
+
+const Title = styled(Flex)`
+ font-size: 16px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ cursor: pointer;
+ width: 0;
+
+ ${breakpoint("tablet")`
+ flex-grow: 1;
+ `};
+`;
+
+const Actions = styled(Flex)`
+ align-self: flex-end;
+ height: 32px;
+`;
+
+export default observer(Header);
diff --git a/app/components/Input.js b/app/components/Input.js
index 855f59b49..8ca02b24c 100644
--- a/app/components/Input.js
+++ b/app/components/Input.js
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
+import breakpoint from "styled-components-breakpoint";
import Flex from "components/Flex";
const RealTextarea = styled.textarea`
@@ -33,6 +34,10 @@ const RealInput = styled.input`
&::placeholder {
color: ${(props) => props.theme.placeholder};
}
+
+ ${breakpoint("mobile", "tablet")`
+ font-size: 16px;
+ `};
`;
const Wrapper = styled.div`
diff --git a/app/components/Layout.js b/app/components/Layout.js
index 74b90bea7..0615e4da6 100644
--- a/app/components/Layout.js
+++ b/app/components/Layout.js
@@ -7,7 +7,7 @@ import { Helmet } from "react-helmet";
import { withTranslation, type TFunction } from "react-i18next";
import keydown from "react-keydown";
import { Switch, Route, Redirect } from "react-router-dom";
-import styled, { withTheme } from "styled-components";
+import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import AuthStore from "stores/AuthStore";
import DocumentsStore from "stores/DocumentsStore";
@@ -24,7 +24,6 @@ import Sidebar from "components/Sidebar";
import SettingsSidebar from "components/Sidebar/Settings";
import SkipNavContent from "components/SkipNavContent";
import SkipNavLink from "components/SkipNavLink";
-import { type Theme } from "types";
import { meta } from "utils/keyboard";
import {
homeUrl,
@@ -40,7 +39,6 @@ type Props = {
auth: AuthStore,
ui: UiStore,
notifications?: React.Node,
- theme: Theme,
i18n: Object,
t: TFunction,
};
@@ -51,24 +49,12 @@ class Layout extends React.Component {
@observable redirectTo: ?string;
@observable keyboardShortcutsOpen: boolean = false;
- constructor(props: Props) {
- super();
- this.updateBackground(props);
- }
-
componentDidUpdate() {
- this.updateBackground(this.props);
-
if (this.redirectTo) {
this.redirectTo = undefined;
}
}
- updateBackground(props: Props) {
- // ensure the wider page color always matches the theme
- window.document.body.style.background = props.theme.background;
- }
-
@keydown(`${meta}+.`)
handleToggleSidebar() {
this.props.ui.toggleCollapsedSidebar();
@@ -212,5 +198,5 @@ const Content = styled(Flex)`
`;
export default withTranslation()(
- inject("auth", "ui", "documents")(withTheme(Layout))
+ inject("auth", "ui", "documents")(Layout)
);
diff --git a/app/components/PageTheme.js b/app/components/PageTheme.js
new file mode 100644
index 000000000..2914f78b2
--- /dev/null
+++ b/app/components/PageTheme.js
@@ -0,0 +1,35 @@
+// @flow
+import * as React from "react";
+import { useTheme } from "styled-components";
+import useStores from "hooks/useStores";
+
+export default function PageTheme() {
+ const { ui } = useStores();
+ const theme = useTheme();
+
+ React.useEffect(() => {
+ // wider page background beyond the React root
+ if (document.body) {
+ document.body.style.background = theme.background;
+ }
+
+ // theme-color adjusts the title bar color for desktop PWA
+ const themeElement = document.querySelector('meta[name="theme-color"]');
+ if (themeElement) {
+ themeElement.setAttribute("content", theme.background);
+ }
+
+ // status bar color for iOS PWA
+ const statusElement = document.querySelector(
+ 'meta[name="apple-mobile-web-app-status-bar-style"]'
+ );
+ if (statusElement) {
+ statusElement.setAttribute(
+ "content",
+ ui.resolvedTheme === "dark" ? "black-translucent" : "default"
+ );
+ }
+ }, [theme, ui.resolvedTheme]);
+
+ return null;
+}
diff --git a/app/components/Scene.js b/app/components/Scene.js
new file mode 100644
index 000000000..eaf31732b
--- /dev/null
+++ b/app/components/Scene.js
@@ -0,0 +1,50 @@
+// @flow
+import * as React from "react";
+import styled from "styled-components";
+import CenteredContent from "components/CenteredContent";
+import Header from "components/Header";
+import PageTitle from "components/PageTitle";
+
+type Props = {|
+ icon?: React.Node,
+ title: React.Node,
+ textTitle?: string,
+ children: React.Node,
+ breadcrumb?: React.Node,
+ actions?: React.Node,
+|};
+
+function Scene({
+ title,
+ icon,
+ textTitle,
+ actions,
+ breadcrumb,
+ children,
+}: Props) {
+ return (
+
+
+
+ {icon} {title}
+ >
+ ) : (
+ title
+ )
+ }
+ actions={actions}
+ breadcrumb={breadcrumb}
+ />
+ {children}
+
+ );
+}
+
+const FillWidth = styled.div`
+ width: 100%;
+`;
+
+export default Scene;
diff --git a/app/components/Sidebar/Main.js b/app/components/Sidebar/Main.js
index d66967256..8c948ccac 100644
--- a/app/components/Sidebar/Main.js
+++ b/app/components/Sidebar/Main.js
@@ -22,9 +22,9 @@ import Modal from "components/Modal";
import Scrollable from "components/Scrollable";
import Sidebar from "./Sidebar";
import Collections from "./components/Collections";
-import HeaderBlock from "./components/HeaderBlock";
import Section from "./components/Section";
import SidebarLink from "./components/SidebarLink";
+import TeamButton from "./components/TeamButton";
import useStores from "hooks/useStores";
import AccountMenu from "menus/AccountMenu";
@@ -72,7 +72,7 @@ function MainSidebar() {
{(props) => (
-
-
{t("Return to App")}
diff --git a/app/components/Sidebar/components/HeaderBlock.js b/app/components/Sidebar/components/TeamButton.js
similarity index 88%
rename from app/components/Sidebar/components/HeaderBlock.js
rename to app/components/Sidebar/components/TeamButton.js
index ee8e40e46..9a9206dd1 100644
--- a/app/components/Sidebar/components/HeaderBlock.js
+++ b/app/components/Sidebar/components/TeamButton.js
@@ -13,7 +13,7 @@ type Props = {|
logoUrl: string,
|};
-const HeaderBlock = React.forwardRef(
+const TeamButton = React.forwardRef(
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
@@ -25,8 +25,7 @@ const HeaderBlock = React.forwardRef(
/>
- {teamName}{" "}
- {showDisclosure && }
+ {teamName} {showDisclosure && }
{subheading}
@@ -35,7 +34,7 @@ const HeaderBlock = React.forwardRef(
)
);
-const StyledExpandedIcon = styled(ExpandedIcon)`
+const Disclosure = styled(ExpandedIcon)`
position: absolute;
right: 0;
top: 0;
@@ -84,4 +83,4 @@ const Header = styled.button`
}
`;
-export default HeaderBlock;
+export default TeamButton;
diff --git a/app/components/Sidebar/components/Toggle.js b/app/components/Sidebar/components/Toggle.js
index 4533c5206..86062e31a 100644
--- a/app/components/Sidebar/components/Toggle.js
+++ b/app/components/Sidebar/components/Toggle.js
@@ -2,6 +2,7 @@
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
+import Arrow from "components/Arrow";
type Props = {
direction: "left" | "right",
@@ -14,22 +15,7 @@ const Toggle = React.forwardRef(
return (
-
+
);
@@ -60,6 +46,7 @@ export const ToggleButton = styled.button`
`;
export const Positioner = styled.div`
+ display: none;
z-index: 2;
position: absolute;
top: 0;
@@ -70,6 +57,10 @@ export const Positioner = styled.div`
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
opacity: 1;
}
+
+ ${breakpoint("tablet")`
+ display: block;
+ `}
`;
export default Toggle;
diff --git a/app/components/Subheading.js b/app/components/Subheading.js
index c0716deb5..eecb044ab 100644
--- a/app/components/Subheading.js
+++ b/app/components/Subheading.js
@@ -2,19 +2,17 @@
import * as React from "react";
import styled from "styled-components";
-type Props = {
+type Props = {|
children: React.Node,
-};
+|};
const H3 = styled.h3`
border-bottom: 1px solid ${(props) => props.theme.divider};
- margin-top: 22px;
- margin-bottom: 12px;
+ margin: 12px 0;
line-height: 1;
- position: relative;
`;
-const Underline = styled("span")`
+const Underline = styled.div`
margin-top: -1px;
display: inline-block;
font-weight: 500;
@@ -22,14 +20,29 @@ const Underline = styled("span")`
line-height: 1.5;
color: ${(props) => props.theme.textSecondary};
border-bottom: 3px solid ${(props) => props.theme.textSecondary};
- padding-bottom: 5px;
+ padding-top: 6px;
+ padding-bottom: 4px;
+`;
+
+// When sticky we need extra background coverage around the sides otherwise
+// items that scroll past can "stick out" the sides of the heading
+const Sticky = styled.div`
+ position: sticky;
+ top: 54px;
+ margin: 0 -8px;
+ padding: 0 8px;
+ background: ${(props) => props.theme.background};
+ transition: ${(props) => props.theme.backgroundTransition};
+ z-index: 1;
`;
const Subheading = ({ children, ...rest }: Props) => {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
};
diff --git a/app/components/Tab.js b/app/components/Tab.js
index c616e0a3e..5d7717445 100644
--- a/app/components/Tab.js
+++ b/app/components/Tab.js
@@ -8,7 +8,7 @@ type Props = {
theme: Theme,
};
-const StyledNavLink = styled(NavLink)`
+const TabLink = styled(NavLink)`
position: relative;
display: inline-flex;
align-items: center;
@@ -16,7 +16,7 @@ const StyledNavLink = styled(NavLink)`
font-size: 14px;
color: ${(props) => props.theme.textTertiary};
margin-right: 24px;
- padding-bottom: 8px;
+ padding: 6px 0;
&:hover {
color: ${(props) => props.theme.textSecondary};
@@ -32,7 +32,7 @@ function Tab({ theme, ...rest }: Props) {
color: theme.textSecondary,
};
- return ;
+ return ;
}
export default withTheme(Tab);
diff --git a/app/components/Tabs.js b/app/components/Tabs.js
index 5ce46c3db..1c6c5b482 100644
--- a/app/components/Tabs.js
+++ b/app/components/Tabs.js
@@ -1,16 +1,27 @@
// @flow
+import * as React from "react";
import styled from "styled-components";
-const Tabs = styled.nav`
- position: relative;
+const Nav = styled.nav`
border-bottom: 1px solid ${(props) => props.theme.divider};
- margin-top: 22px;
- margin-bottom: 12px;
+ margin: 12px 0;
overflow-y: auto;
white-space: nowrap;
transition: opacity 250ms ease-out;
`;
+// When sticky we need extra background coverage around the sides otherwise
+// items that scroll past can "stick out" the sides of the heading
+const Sticky = styled.div`
+ position: sticky;
+ top: 54px;
+ margin: 0 -8px;
+ padding: 0 8px;
+ background: ${(props) => props.theme.background};
+ transition: ${(props) => props.theme.backgroundTransition};
+ z-index: 1;
+`;
+
export const Separator = styled.span`
border-left: 1px solid ${(props) => props.theme.divider};
position: relative;
@@ -19,4 +30,12 @@ export const Separator = styled.span`
margin-top: 6px;
`;
+const Tabs = (props: {}) => {
+ return (
+
+
+
+ );
+};
+
export default Tabs;
diff --git a/app/components/Toasts/components/Toast.js b/app/components/Toast.js
similarity index 100%
rename from app/components/Toasts/components/Toast.js
rename to app/components/Toast.js
diff --git a/app/components/Toasts/Toasts.js b/app/components/Toasts.js
similarity index 94%
rename from app/components/Toasts/Toasts.js
rename to app/components/Toasts.js
index c82bedea3..df2502fdc 100644
--- a/app/components/Toasts/Toasts.js
+++ b/app/components/Toasts.js
@@ -2,7 +2,7 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
-import Toast from "./components/Toast";
+import Toast from "components/Toast";
import useStores from "hooks/useStores";
function Toasts() {
diff --git a/app/components/Toasts/index.js b/app/components/Toasts/index.js
deleted file mode 100644
index 13373bf82..000000000
--- a/app/components/Toasts/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// @flow
-import Toasts from "./Toasts";
-export default Toasts;
diff --git a/app/hooks/useDebouncedCallback.js b/app/hooks/useDebouncedCallback.js
new file mode 100644
index 000000000..9ba68d327
--- /dev/null
+++ b/app/hooks/useDebouncedCallback.js
@@ -0,0 +1,31 @@
+// @flow
+import * as React from "react";
+
+export default function useDebouncedCallback(
+ callback: (any) => mixed,
+ wait: number
+) {
+ // track args & timeout handle between calls
+ const argsRef = React.useRef();
+ const timeout = React.useRef();
+
+ function cleanup() {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ }
+ }
+
+ // make sure our timeout gets cleared if consuming component gets unmounted
+ React.useEffect(() => cleanup, []);
+
+ return function (...args: any) {
+ argsRef.current = args;
+ cleanup();
+
+ timeout.current = setTimeout(() => {
+ if (argsRef.current) {
+ callback(...argsRef.current);
+ }
+ }, wait);
+ };
+}
diff --git a/app/index.js b/app/index.js
index c991c9cdc..3fab57f9a 100644
--- a/app/index.js
+++ b/app/index.js
@@ -10,6 +10,7 @@ import { Router } from "react-router-dom";
import { initI18n } from "shared/i18n";
import stores from "stores";
import ErrorBoundary from "components/ErrorBoundary";
+import PageTheme from "components/PageTheme";
import ScrollToTop from "components/ScrollToTop";
import Theme from "components/Theme";
import Toasts from "components/Toasts";
@@ -19,13 +20,28 @@ import { initSentry } from "utils/sentry";
initI18n();
-const element = document.getElementById("root");
+const element = window.document.getElementById("root");
const history = createBrowserHistory();
if (env.SENTRY_DSN) {
initSentry(history);
}
+if ("serviceWorker" in window.navigator) {
+ window.addEventListener("load", () => {
+ window.navigator.serviceWorker
+ .register("/static/service-worker.js", {
+ scope: "/",
+ })
+ .then((registration) => {
+ console.log("SW registered: ", registration);
+ })
+ .catch((registrationError) => {
+ console.log("SW registration failed: ", registrationError);
+ });
+ });
+}
+
if (element) {
render(
@@ -34,6 +50,7 @@ if (element) {
<>
+
diff --git a/app/routes/authenticated.js b/app/routes/authenticated.js
index 7a1ef1758..2168bac50 100644
--- a/app/routes/authenticated.js
+++ b/app/routes/authenticated.js
@@ -3,11 +3,11 @@ import * as React from "react";
import { Switch, 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 Home from "scenes/Home";
import Search from "scenes/Search";
import Starred from "scenes/Starred";
import Templates from "scenes/Templates";
@@ -37,8 +37,8 @@ export default function AuthenticatedRoutes() {
-
-
+
+
diff --git a/app/scenes/Archive.js b/app/scenes/Archive.js
index 25394cec3..3290ff71e 100644
--- a/app/scenes/Archive.js
+++ b/app/scenes/Archive.js
@@ -20,7 +20,7 @@ function Archive(props: Props) {
const { documents } = props;
return (
-
+
{t("Archive")}
{
@observable isFetching: boolean = true;
@observable permissionsModalOpen: boolean = false;
@observable editModalOpen: boolean = false;
- @observable redirectTo: ?string;
componentDidMount() {
const { id } = this.props.match.params;
@@ -108,14 +103,6 @@ class CollectionScene extends React.Component {
}
};
- onNewDocument = (ev: SyntheticEvent<>) => {
- ev.preventDefault();
-
- if (this.collection) {
- this.redirectTo = newDocumentUrl(this.collection.id);
- }
- };
-
onPermissions = (ev: SyntheticEvent<>) => {
ev.preventDefault();
this.permissionsModalOpen = true;
@@ -138,7 +125,7 @@ class CollectionScene extends React.Component {
const can = policies.abilities(match.params.id || "");
return (
-
+ <>
{can.update && (
<>
@@ -157,7 +144,12 @@ class CollectionScene extends React.Component {
delay={500}
placement="bottom"
>
- }>
+ }
+ >
{t("New doc")}
@@ -181,14 +173,13 @@ class CollectionScene extends React.Component {
)}
/>
-
+ >
);
}
render() {
- const { documents, theme, t } = this.props;
+ const { documents, t } = this.props;
- if (this.redirectTo) return ;
if (!this.isFetching && !this.collection) return ;
const pinnedDocuments = this.collection
@@ -197,181 +188,171 @@ class CollectionScene extends React.Component {
const collection = this.collection;
const collectionName = collection ? collection.name : "";
const hasPinnedDocuments = !!pinnedDocuments.length;
- const hasDescription = collection ? collection.hasDescription : false;
- return (
-
- {collection ? (
+ return collection ? (
+
-
- {collection.isEmpty ? (
-
-
- }}
- />
-
- Get started by creating a new one!
-
-
-
- }>
- {t("Create a document")}
-
-
-
- {collection.private && (
-
- )}
-
-
-
-
-
-
-
-
- ) : (
- <>
-
- {" "}
- {collection.name}
-
-
- {hasDescription && (
- Loading…
}>
-
-
- )}
-
- {hasPinnedDocuments && (
- <>
-
- {t("Pinned")}
-
-
- >
- )}
-
-
-
- {t("Documents")}
-
-
- {t("Recently updated")}
-
-
- {t("Recently published")}
-
-
- {t("Least recently updated")}
-
-
- {t("A–Z")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
- {this.renderActions()}
+
+
+ {collection.name}
>
+ }
+ actions={this.renderActions()}
+ >
+ {collection.isEmpty ? (
+
+
+ }}
+ />
+
+ Get started by creating a new one!
+
+
+
+ }>
+ {t("Create a document")}
+
+
+
+ {collection.private && (
+
+ )}
+
+
+
+
+
+
+
+
) : (
<>
-
+ {" "}
+ {collection.name}
-
+
+
+ {hasPinnedDocuments && (
+ <>
+
+ {t("Pinned")}
+
+
+ >
+ )}
+
+
+
+ {t("Documents")}
+
+
+ {t("Recently updated")}
+
+
+ {t("Recently published")}
+
+
+ {t("Least recently updated")}
+
+
+ {t("A–Z")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
)}
+
+ ) : (
+
+
+
+
+
);
}
@@ -390,16 +371,11 @@ const TinyPinIcon = styled(PinIcon)`
opacity: 0.8;
`;
-const Wrapper = styled(Flex)`
+const Empty = styled(Flex)`
justify-content: center;
margin: 10px 0;
`;
export default withTranslation()(
- inject(
- "collections",
- "policies",
- "documents",
- "ui"
- )(withTheme(CollectionScene))
+ inject("collections", "policies", "documents", "ui")(CollectionScene)
);
diff --git a/app/scenes/CollectionEdit.js b/app/scenes/CollectionEdit.js
index 84ede8337..a759821ad 100644
--- a/app/scenes/CollectionEdit.js
+++ b/app/scenes/CollectionEdit.js
@@ -11,7 +11,6 @@ import Flex from "components/Flex";
import HelpText from "components/HelpText";
import IconPicker from "components/IconPicker";
import Input from "components/Input";
-import InputRich from "components/InputRich";
import InputSelect from "components/InputSelect";
import Switch from "components/Switch";
@@ -27,7 +26,6 @@ type Props = {
class CollectionEdit extends React.Component {
@observable name: string = this.props.collection.name;
@observable sharing: boolean = this.props.collection.sharing;
- @observable description: string = this.props.collection.description;
@observable icon: string = this.props.collection.icon;
@observable color: string = this.props.collection.color || "#4E5C6E";
@observable private: boolean = this.props.collection.private;
@@ -43,7 +41,6 @@ class CollectionEdit extends React.Component {
try {
await this.props.collection.save({
name: this.name,
- description: this.description,
icon: this.icon,
color: this.color,
private: this.private,
@@ -69,10 +66,6 @@ class CollectionEdit extends React.Component {
}
};
- handleDescriptionChange = (getValue: () => string) => {
- this.description = getValue();
- };
-
handleNameChange = (ev: SyntheticInputEvent<*>) => {
this.name = ev.target.value;
};
@@ -120,15 +113,6 @@ class CollectionEdit extends React.Component {
icon={this.icon}
/>
-
{
@observable name: string = "";
- @observable description: string = "";
@observable icon: string = "";
@observable color: string = "#4E5C6E";
@observable sharing: boolean = true;
@@ -43,7 +41,6 @@ class CollectionNew extends React.Component {
const collection = new Collection(
{
name: this.name,
- description: this.description,
sharing: this.sharing,
icon: this.icon,
color: this.color,
@@ -90,10 +87,6 @@ class CollectionNew extends React.Component {
this.hasOpenedIconPicker = true;
};
- handleDescriptionChange = (getValue: () => string) => {
- this.description = getValue();
- };
-
handlePrivateChange = (ev: SyntheticInputEvent) => {
this.private = ev.target.checked;
};
@@ -115,9 +108,9 @@ class CollectionNew extends React.Component {