Merge branch 'main' into feat/mass-import
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<i>An open, extensible, wiki for your team built using React and Node.js.<br/>Try out Outline using our hosted version at <a href="https://www.getoutline.com">www.getoutline.com</a>.</i>
|
<i>An open, extensible, wiki for your team built using React and Node.js.<br/>Try out Outline using our hosted version at <a href="https://www.getoutline.com">www.getoutline.com</a>.</i>
|
||||||
<br/>
|
<br/>
|
||||||
<img src="https://user-images.githubusercontent.com/380914/78513257-153ae080-775f-11ea-9b49-1e1939451a3e.png" alt="Outline" width="800" />
|
<img src="https://www.getoutline.com/images/screenshot@2x.png" alt="Outline" width="800" />
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a>
|
<a href="https://circleci.com/gh/outline/outline" rel="nofollow"><img src="https://circleci.com/gh/outline/outline.svg?style=shield&circle-token=c0c4c2f39990e277385d5c1ae96169c409eb887a"></a>
|
||||||
|
|||||||
6
app.json
6
app.json
@@ -30,6 +30,10 @@
|
|||||||
"postdeploy": "yarn sequelize db:migrate"
|
"postdeploy": "yarn sequelize db:migrate"
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
|
"NODE_ENV": {
|
||||||
|
"value": "production",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
"SECRET_KEY": {
|
"SECRET_KEY": {
|
||||||
"description": "A secret key",
|
"description": "A secret key",
|
||||||
"generator": "secret",
|
"generator": "secret",
|
||||||
@@ -144,4 +148,4 @@
|
|||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/components/Arrow.js
Normal file
23
app/components/Arrow.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export default function Arrow() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 13 30"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,17 +3,18 @@ import * as React from "react";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
};
|
withStickyHeader?: boolean,
|
||||||
|
|};
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
padding: 60px 20px;
|
padding: ${(props) => (props.withStickyHeader ? "4px 12px" : "60px 12px")};
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
${breakpoint("tablet")`
|
||||||
padding: 60px;
|
padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import { sortBy, keyBy } from "lodash";
|
import { sortBy, keyBy } from "lodash";
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
import * as React from "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 { MAX_AVATAR_DISPLAY } from "shared/constants";
|
||||||
|
|
||||||
import DocumentPresenceStore from "stores/DocumentPresenceStore";
|
import DocumentPresenceStore from "stores/DocumentPresenceStore";
|
||||||
import ViewsStore from "stores/ViewsStore";
|
import ViewsStore from "stores/ViewsStore";
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
@@ -51,7 +52,7 @@ class Collaborators extends React.Component<Props> {
|
|||||||
const overflow = documentViews.length - mostRecentViewers.length;
|
const overflow = documentViews.length - mostRecentViewers.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Facepile
|
<FacepileHiddenOnMobile
|
||||||
users={mostRecentViewers.map((v) => v.user)}
|
users={mostRecentViewers.map((v) => v.user)}
|
||||||
overflow={overflow}
|
overflow={overflow}
|
||||||
renderAvatar={(user) => {
|
renderAvatar={(user) => {
|
||||||
@@ -75,4 +76,10 @@ class Collaborators extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FacepileHiddenOnMobile = styled(Facepile)`
|
||||||
|
${breakpoint("mobile", "tablet")`
|
||||||
|
display: none;
|
||||||
|
`};
|
||||||
|
`;
|
||||||
|
|
||||||
export default inject("views", "presence")(Collaborators);
|
export default inject("views", "presence")(Collaborators);
|
||||||
|
|||||||
212
app/components/CollectionDescription.js
Normal file
212
app/components/CollectionDescription.js
Normal file
@@ -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 (
|
||||||
|
<MaxHeight data-editing={isEditing} data-expanded={isExpanded}>
|
||||||
|
<Input
|
||||||
|
$isEditable={can.update}
|
||||||
|
data-editing={isEditing}
|
||||||
|
data-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
<span onClick={can.update ? handleStartEditing : undefined}>
|
||||||
|
{collections.isSaving && <LoadingIndicator />}
|
||||||
|
{collection.hasDescription || isEditing || isDirty ? (
|
||||||
|
<React.Suspense fallback={<Placeholder>Loading…</Placeholder>}>
|
||||||
|
<Editor
|
||||||
|
id={collection.id}
|
||||||
|
key={key}
|
||||||
|
defaultValue={collection.description || ""}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
readOnly={!isEditing}
|
||||||
|
autoFocus={isEditing}
|
||||||
|
onBlur={handleStopEditing}
|
||||||
|
maxLength={1000}
|
||||||
|
disableEmbeds
|
||||||
|
readOnlyWriteCheckboxes
|
||||||
|
grow
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
) : (
|
||||||
|
can.update && <Placeholder>{placeholder}</Placeholder>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Input>
|
||||||
|
{!isEditing && (
|
||||||
|
<Disclosure
|
||||||
|
onClick={handleClickDisclosure}
|
||||||
|
aria-label={isExpanded ? t("Collapse") : t("Expand")}
|
||||||
|
size={30}
|
||||||
|
>
|
||||||
|
<Arrow />
|
||||||
|
</Disclosure>
|
||||||
|
)}
|
||||||
|
</MaxHeight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -163,8 +163,11 @@ const DocumentLink = styled(Link)`
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
min-width: 100%;
|
width: calc(100vw - 8px);
|
||||||
max-width: calc(100vw - 40px);
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
width: auto;
|
||||||
|
`};
|
||||||
|
|
||||||
${Actions} {
|
${Actions} {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -27,13 +27,16 @@ export type Props = {|
|
|||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
template?: boolean,
|
template?: boolean,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
|
maxLength?: number,
|
||||||
scrollTo?: string,
|
scrollTo?: string,
|
||||||
|
handleDOMEvents?: Object,
|
||||||
readOnlyWriteCheckboxes?: boolean,
|
readOnlyWriteCheckboxes?: boolean,
|
||||||
onBlur?: (event: SyntheticEvent<>) => any,
|
onBlur?: (event: SyntheticEvent<>) => any,
|
||||||
onFocus?: (event: SyntheticEvent<>) => any,
|
onFocus?: (event: SyntheticEvent<>) => any,
|
||||||
onPublish?: (event: SyntheticEvent<>) => any,
|
onPublish?: (event: SyntheticEvent<>) => any,
|
||||||
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
onSave?: ({ done?: boolean, autosave?: boolean, publish?: boolean }) => any,
|
||||||
onCancel?: () => any,
|
onCancel?: () => any,
|
||||||
|
onDoubleClick?: () => any,
|
||||||
onChange?: (getValue: () => string) => any,
|
onChange?: (getValue: () => string) => any,
|
||||||
onSearchLink?: (title: string) => any,
|
onSearchLink?: (title: string) => any,
|
||||||
onHoverLink?: (event: MouseEvent) => any,
|
onHoverLink?: (event: MouseEvent) => any,
|
||||||
@@ -177,7 +180,7 @@ const StyledEditor = styled(RichMarkdownEditor)`
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
transition: ${(props) => props.theme.backgroundTransition};
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& * {
|
& * {
|
||||||
|
|||||||
104
app/components/Header.js
Normal file
104
app/components/Header.js
Normal file
@@ -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 (
|
||||||
|
<Wrapper
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
isCompact={isScrolled}
|
||||||
|
shrink={false}
|
||||||
|
>
|
||||||
|
{breadcrumb}
|
||||||
|
{isScrolled ? (
|
||||||
|
<Title
|
||||||
|
align="center"
|
||||||
|
justify={breadcrumb ? "center" : "flex-start"}
|
||||||
|
onClick={handleClickTitle}
|
||||||
|
>
|
||||||
|
<Fade>
|
||||||
|
<Flex align="center">{title}</Flex>
|
||||||
|
</Fade>
|
||||||
|
</Title>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
{actions && <Actions>{actions}</Actions>}
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -4,6 +4,7 @@ import { observer } from "mobx-react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
|
|
||||||
const RealTextarea = styled.textarea`
|
const RealTextarea = styled.textarea`
|
||||||
@@ -33,6 +34,10 @@ const RealInput = styled.input`
|
|||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: ${(props) => props.theme.placeholder};
|
color: ${(props) => props.theme.placeholder};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${breakpoint("mobile", "tablet")`
|
||||||
|
font-size: 16px;
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Helmet } from "react-helmet";
|
|||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { withTranslation, type TFunction } from "react-i18next";
|
||||||
import keydown from "react-keydown";
|
import keydown from "react-keydown";
|
||||||
import { Switch, Route, Redirect } from "react-router-dom";
|
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 breakpoint from "styled-components-breakpoint";
|
||||||
import AuthStore from "stores/AuthStore";
|
import AuthStore from "stores/AuthStore";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
@@ -24,7 +24,6 @@ import Sidebar from "components/Sidebar";
|
|||||||
import SettingsSidebar from "components/Sidebar/Settings";
|
import SettingsSidebar from "components/Sidebar/Settings";
|
||||||
import SkipNavContent from "components/SkipNavContent";
|
import SkipNavContent from "components/SkipNavContent";
|
||||||
import SkipNavLink from "components/SkipNavLink";
|
import SkipNavLink from "components/SkipNavLink";
|
||||||
import { type Theme } from "types";
|
|
||||||
import { meta } from "utils/keyboard";
|
import { meta } from "utils/keyboard";
|
||||||
import {
|
import {
|
||||||
homeUrl,
|
homeUrl,
|
||||||
@@ -40,7 +39,6 @@ type Props = {
|
|||||||
auth: AuthStore,
|
auth: AuthStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
notifications?: React.Node,
|
notifications?: React.Node,
|
||||||
theme: Theme,
|
|
||||||
i18n: Object,
|
i18n: Object,
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
};
|
};
|
||||||
@@ -51,24 +49,12 @@ class Layout extends React.Component<Props> {
|
|||||||
@observable redirectTo: ?string;
|
@observable redirectTo: ?string;
|
||||||
@observable keyboardShortcutsOpen: boolean = false;
|
@observable keyboardShortcutsOpen: boolean = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super();
|
|
||||||
this.updateBackground(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
this.updateBackground(this.props);
|
|
||||||
|
|
||||||
if (this.redirectTo) {
|
if (this.redirectTo) {
|
||||||
this.redirectTo = undefined;
|
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}+.`)
|
@keydown(`${meta}+.`)
|
||||||
handleToggleSidebar() {
|
handleToggleSidebar() {
|
||||||
this.props.ui.toggleCollapsedSidebar();
|
this.props.ui.toggleCollapsedSidebar();
|
||||||
@@ -212,5 +198,5 @@ const Content = styled(Flex)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<Layout>(
|
export default withTranslation()<Layout>(
|
||||||
inject("auth", "ui", "documents")(withTheme(Layout))
|
inject("auth", "ui", "documents")(Layout)
|
||||||
);
|
);
|
||||||
|
|||||||
35
app/components/PageTheme.js
Normal file
35
app/components/PageTheme.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
50
app/components/Scene.js
Normal file
50
app/components/Scene.js
Normal file
@@ -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 (
|
||||||
|
<FillWidth>
|
||||||
|
<PageTitle title={textTitle || title} />
|
||||||
|
<Header
|
||||||
|
title={
|
||||||
|
icon ? (
|
||||||
|
<>
|
||||||
|
{icon} {title}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
actions={actions}
|
||||||
|
breadcrumb={breadcrumb}
|
||||||
|
/>
|
||||||
|
<CenteredContent withStickyHeader>{children}</CenteredContent>
|
||||||
|
</FillWidth>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FillWidth = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Scene;
|
||||||
@@ -22,9 +22,9 @@ import Modal from "components/Modal";
|
|||||||
import Scrollable from "components/Scrollable";
|
import Scrollable from "components/Scrollable";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Collections from "./components/Collections";
|
import Collections from "./components/Collections";
|
||||||
import HeaderBlock from "./components/HeaderBlock";
|
|
||||||
import Section from "./components/Section";
|
import Section from "./components/Section";
|
||||||
import SidebarLink from "./components/SidebarLink";
|
import SidebarLink from "./components/SidebarLink";
|
||||||
|
import TeamButton from "./components/TeamButton";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
import AccountMenu from "menus/AccountMenu";
|
import AccountMenu from "menus/AccountMenu";
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ function MainSidebar() {
|
|||||||
<Sidebar>
|
<Sidebar>
|
||||||
<AccountMenu>
|
<AccountMenu>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<HeaderBlock
|
<TeamButton
|
||||||
{...props}
|
{...props}
|
||||||
subheading={user.name}
|
subheading={user.name}
|
||||||
teamName={team.name}
|
teamName={team.name}
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import Scrollable from "components/Scrollable";
|
|||||||
|
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import HeaderBlock from "./components/HeaderBlock";
|
|
||||||
import Section from "./components/Section";
|
import Section from "./components/Section";
|
||||||
import SidebarLink from "./components/SidebarLink";
|
import SidebarLink from "./components/SidebarLink";
|
||||||
|
import TeamButton from "./components/TeamButton";
|
||||||
import Version from "./components/Version";
|
import Version from "./components/Version";
|
||||||
import SlackIcon from "./icons/Slack";
|
import SlackIcon from "./icons/Slack";
|
||||||
import ZapierIcon from "./icons/Zapier";
|
import ZapierIcon from "./icons/Zapier";
|
||||||
@@ -46,7 +46,7 @@ function SettingsSidebar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<HeaderBlock
|
<TeamButton
|
||||||
subheading={
|
subheading={
|
||||||
<ReturnToApp align="center">
|
<ReturnToApp align="center">
|
||||||
<BackIcon color="currentColor" /> {t("Return to App")}
|
<BackIcon color="currentColor" /> {t("Return to App")}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Props = {|
|
|||||||
logoUrl: string,
|
logoUrl: string,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
const HeaderBlock = React.forwardRef<Props, any>(
|
const TeamButton = React.forwardRef<Props, any>(
|
||||||
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
|
({ showDisclosure, teamName, subheading, logoUrl, ...rest }: Props, ref) => (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Header justify="flex-start" align="center" ref={ref} {...rest}>
|
<Header justify="flex-start" align="center" ref={ref} {...rest}>
|
||||||
@@ -25,8 +25,7 @@ const HeaderBlock = React.forwardRef<Props, any>(
|
|||||||
/>
|
/>
|
||||||
<Flex align="flex-start" column>
|
<Flex align="flex-start" column>
|
||||||
<TeamName showDisclosure>
|
<TeamName showDisclosure>
|
||||||
{teamName}{" "}
|
{teamName} {showDisclosure && <Disclosure color="currentColor" />}
|
||||||
{showDisclosure && <StyledExpandedIcon color="currentColor" />}
|
|
||||||
</TeamName>
|
</TeamName>
|
||||||
<Subheading>{subheading}</Subheading>
|
<Subheading>{subheading}</Subheading>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -35,7 +34,7 @@ const HeaderBlock = React.forwardRef<Props, any>(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const StyledExpandedIcon = styled(ExpandedIcon)`
|
const Disclosure = styled(ExpandedIcon)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -84,4 +83,4 @@ const Header = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default HeaderBlock;
|
export default TeamButton;
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
|
import Arrow from "components/Arrow";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
@@ -14,22 +15,7 @@ const Toggle = React.forwardRef<Props, HTMLButtonElement>(
|
|||||||
return (
|
return (
|
||||||
<Positioner style={style}>
|
<Positioner style={style}>
|
||||||
<ToggleButton ref={ref} $direction={direction} onClick={onClick}>
|
<ToggleButton ref={ref} $direction={direction} onClick={onClick}>
|
||||||
<svg
|
<Arrow />
|
||||||
width="13"
|
|
||||||
height="30"
|
|
||||||
viewBox="0 0 13 30"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</Positioner>
|
</Positioner>
|
||||||
);
|
);
|
||||||
@@ -60,6 +46,7 @@ export const ToggleButton = styled.button`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Positioner = styled.div`
|
export const Positioner = styled.div`
|
||||||
|
display: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -70,6 +57,10 @@ export const Positioner = styled.div`
|
|||||||
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
|
&:hover ${ToggleButton}, &:focus-within ${ToggleButton} {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${breakpoint("tablet")`
|
||||||
|
display: block;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Toggle;
|
export default Toggle;
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
children: React.Node,
|
children: React.Node,
|
||||||
};
|
|};
|
||||||
|
|
||||||
const H3 = styled.h3`
|
const H3 = styled.h3`
|
||||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||||
margin-top: 22px;
|
margin: 12px 0;
|
||||||
margin-bottom: 12px;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
position: relative;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Underline = styled("span")`
|
const Underline = styled.div`
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -22,14 +20,29 @@ const Underline = styled("span")`
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: ${(props) => props.theme.textSecondary};
|
color: ${(props) => props.theme.textSecondary};
|
||||||
border-bottom: 3px solid ${(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) => {
|
const Subheading = ({ children, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<H3 {...rest}>
|
<Sticky>
|
||||||
<Underline>{children}</Underline>
|
<H3 {...rest}>
|
||||||
</H3>
|
<Underline>{children}</Underline>
|
||||||
|
</H3>
|
||||||
|
</Sticky>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Props = {
|
|||||||
theme: Theme,
|
theme: Theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledNavLink = styled(NavLink)`
|
const TabLink = styled(NavLink)`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -16,7 +16,7 @@ const StyledNavLink = styled(NavLink)`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: ${(props) => props.theme.textTertiary};
|
color: ${(props) => props.theme.textTertiary};
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
padding-bottom: 8px;
|
padding: 6px 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.textSecondary};
|
color: ${(props) => props.theme.textSecondary};
|
||||||
@@ -32,7 +32,7 @@ function Tab({ theme, ...rest }: Props) {
|
|||||||
color: theme.textSecondary,
|
color: theme.textSecondary,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <StyledNavLink {...rest} activeStyle={activeStyle} />;
|
return <TabLink {...rest} activeStyle={activeStyle} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(Tab);
|
export default withTheme(Tab);
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Tabs = styled.nav`
|
const Nav = styled.nav`
|
||||||
position: relative;
|
|
||||||
border-bottom: 1px solid ${(props) => props.theme.divider};
|
border-bottom: 1px solid ${(props) => props.theme.divider};
|
||||||
margin-top: 22px;
|
margin: 12px 0;
|
||||||
margin-bottom: 12px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
transition: opacity 250ms ease-out;
|
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`
|
export const Separator = styled.span`
|
||||||
border-left: 1px solid ${(props) => props.theme.divider};
|
border-left: 1px solid ${(props) => props.theme.divider};
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -19,4 +30,12 @@ export const Separator = styled.span`
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Tabs = (props: {}) => {
|
||||||
|
return (
|
||||||
|
<Sticky>
|
||||||
|
<Nav {...props}></Nav>
|
||||||
|
</Sticky>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Tabs;
|
export default Tabs;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Toast from "./components/Toast";
|
import Toast from "components/Toast";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
function Toasts() {
|
function Toasts() {
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Toasts from "./Toasts";
|
|
||||||
export default Toasts;
|
|
||||||
31
app/hooks/useDebouncedCallback.js
Normal file
31
app/hooks/useDebouncedCallback.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
19
app/index.js
19
app/index.js
@@ -10,6 +10,7 @@ import { Router } from "react-router-dom";
|
|||||||
import { initI18n } from "shared/i18n";
|
import { initI18n } from "shared/i18n";
|
||||||
import stores from "stores";
|
import stores from "stores";
|
||||||
import ErrorBoundary from "components/ErrorBoundary";
|
import ErrorBoundary from "components/ErrorBoundary";
|
||||||
|
import PageTheme from "components/PageTheme";
|
||||||
import ScrollToTop from "components/ScrollToTop";
|
import ScrollToTop from "components/ScrollToTop";
|
||||||
import Theme from "components/Theme";
|
import Theme from "components/Theme";
|
||||||
import Toasts from "components/Toasts";
|
import Toasts from "components/Toasts";
|
||||||
@@ -19,13 +20,28 @@ import { initSentry } from "utils/sentry";
|
|||||||
|
|
||||||
initI18n();
|
initI18n();
|
||||||
|
|
||||||
const element = document.getElementById("root");
|
const element = window.document.getElementById("root");
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
if (env.SENTRY_DSN) {
|
if (env.SENTRY_DSN) {
|
||||||
initSentry(history);
|
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) {
|
if (element) {
|
||||||
render(
|
render(
|
||||||
<Provider {...stores}>
|
<Provider {...stores}>
|
||||||
@@ -34,6 +50,7 @@ if (element) {
|
|||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<>
|
<>
|
||||||
|
<PageTheme />
|
||||||
<ScrollToTop>
|
<ScrollToTop>
|
||||||
<Routes />
|
<Routes />
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import * as React from "react";
|
|||||||
import { Switch, Redirect, type Match } from "react-router-dom";
|
import { Switch, Redirect, type Match } from "react-router-dom";
|
||||||
import Archive from "scenes/Archive";
|
import Archive from "scenes/Archive";
|
||||||
import Collection from "scenes/Collection";
|
import Collection from "scenes/Collection";
|
||||||
import Dashboard from "scenes/Dashboard";
|
|
||||||
import KeyedDocument from "scenes/Document/KeyedDocument";
|
import KeyedDocument from "scenes/Document/KeyedDocument";
|
||||||
import DocumentNew from "scenes/DocumentNew";
|
import DocumentNew from "scenes/DocumentNew";
|
||||||
import Drafts from "scenes/Drafts";
|
import Drafts from "scenes/Drafts";
|
||||||
import Error404 from "scenes/Error404";
|
import Error404 from "scenes/Error404";
|
||||||
|
import Home from "scenes/Home";
|
||||||
import Search from "scenes/Search";
|
import Search from "scenes/Search";
|
||||||
import Starred from "scenes/Starred";
|
import Starred from "scenes/Starred";
|
||||||
import Templates from "scenes/Templates";
|
import Templates from "scenes/Templates";
|
||||||
@@ -37,8 +37,8 @@ export default function AuthenticatedRoutes() {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from="/dashboard" to="/home" />
|
<Redirect from="/dashboard" to="/home" />
|
||||||
<Route path="/home/:tab" component={Dashboard} />
|
<Route path="/home/:tab" component={Home} />
|
||||||
<Route path="/home" component={Dashboard} />
|
<Route path="/home" component={Home} />
|
||||||
<Route exact path="/starred" component={Starred} />
|
<Route exact path="/starred" component={Starred} />
|
||||||
<Route exact path="/starred/:sort" component={Starred} />
|
<Route exact path="/starred/:sort" component={Starred} />
|
||||||
<Route exact path="/templates" component={Templates} />
|
<Route exact path="/templates" component={Templates} />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function Archive(props: Props) {
|
|||||||
const { documents } = props;
|
const { documents } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<CenteredContent>
|
||||||
<PageTitle title={t("Archive")} />
|
<PageTitle title={t("Archive")} />
|
||||||
<Heading>{t("Archive")}</Heading>
|
<Heading>{t("Archive")}</Heading>
|
||||||
<PaginatedDocumentList
|
<PaginatedDocumentList
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { observer, inject } from "mobx-react";
|
import { observer, inject } from "mobx-react";
|
||||||
|
|
||||||
import { NewDocumentIcon, PlusIcon, PinIcon, MoreIcon } from "outline-icons";
|
import { NewDocumentIcon, PlusIcon, PinIcon, MoreIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
||||||
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
import { Redirect, Link, Switch, Route, type Match } from "react-router-dom";
|
||||||
import styled, { withTheme } from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import CollectionsStore from "stores/CollectionsStore";
|
import CollectionsStore from "stores/CollectionsStore";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
import UiStore from "stores/UiStore";
|
import UiStore from "stores/UiStore";
|
||||||
import Collection from "models/Collection";
|
import Collection from "models/Collection";
|
||||||
|
|
||||||
import CollectionEdit from "scenes/CollectionEdit";
|
import CollectionEdit from "scenes/CollectionEdit";
|
||||||
import CollectionMembers from "scenes/CollectionMembers";
|
import CollectionMembers from "scenes/CollectionMembers";
|
||||||
import Search from "scenes/Search";
|
import Search from "scenes/Search";
|
||||||
import Actions, { Action, Separator } from "components/Actions";
|
import { Action, Separator } from "components/Actions";
|
||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import CenteredContent from "components/CenteredContent";
|
import CenteredContent from "components/CenteredContent";
|
||||||
|
import CollectionDescription from "components/CollectionDescription";
|
||||||
import CollectionIcon from "components/CollectionIcon";
|
import CollectionIcon from "components/CollectionIcon";
|
||||||
import DocumentList from "components/DocumentList";
|
import DocumentList from "components/DocumentList";
|
||||||
import Editor from "components/Editor";
|
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
@@ -30,14 +28,13 @@ import InputSearch from "components/InputSearch";
|
|||||||
import { ListPlaceholder } from "components/LoadingPlaceholder";
|
import { ListPlaceholder } from "components/LoadingPlaceholder";
|
||||||
import Mask from "components/Mask";
|
import Mask from "components/Mask";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
import PageTitle from "components/PageTitle";
|
|
||||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||||
|
import Scene from "components/Scene";
|
||||||
import Subheading from "components/Subheading";
|
import Subheading from "components/Subheading";
|
||||||
import Tab from "components/Tab";
|
import Tab from "components/Tab";
|
||||||
import Tabs from "components/Tabs";
|
import Tabs from "components/Tabs";
|
||||||
import Tooltip from "components/Tooltip";
|
import Tooltip from "components/Tooltip";
|
||||||
import CollectionMenu from "menus/CollectionMenu";
|
import CollectionMenu from "menus/CollectionMenu";
|
||||||
import { type Theme } from "types";
|
|
||||||
import { AuthorizationError } from "utils/errors";
|
import { AuthorizationError } from "utils/errors";
|
||||||
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
|
import { newDocumentUrl, collectionUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
@@ -47,7 +44,6 @@ type Props = {
|
|||||||
collections: CollectionsStore,
|
collections: CollectionsStore,
|
||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
match: Match,
|
match: Match,
|
||||||
theme: Theme,
|
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,7 +53,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
@observable isFetching: boolean = true;
|
@observable isFetching: boolean = true;
|
||||||
@observable permissionsModalOpen: boolean = false;
|
@observable permissionsModalOpen: boolean = false;
|
||||||
@observable editModalOpen: boolean = false;
|
@observable editModalOpen: boolean = false;
|
||||||
@observable redirectTo: ?string;
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { id } = this.props.match.params;
|
const { id } = this.props.match.params;
|
||||||
@@ -108,14 +103,6 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewDocument = (ev: SyntheticEvent<>) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
if (this.collection) {
|
|
||||||
this.redirectTo = newDocumentUrl(this.collection.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPermissions = (ev: SyntheticEvent<>) => {
|
onPermissions = (ev: SyntheticEvent<>) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.permissionsModalOpen = true;
|
this.permissionsModalOpen = true;
|
||||||
@@ -138,7 +125,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
const can = policies.abilities(match.params.id || "");
|
const can = policies.abilities(match.params.id || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Actions align="center" justify="flex-end">
|
<>
|
||||||
{can.update && (
|
{can.update && (
|
||||||
<>
|
<>
|
||||||
<Action>
|
<Action>
|
||||||
@@ -157,7 +144,12 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
delay={500}
|
delay={500}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<Button onClick={this.onNewDocument} icon={<PlusIcon />}>
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to={this.collection ? newDocumentUrl(this.collection.id) : ""}
|
||||||
|
disabled={!this.collection}
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
>
|
||||||
{t("New doc")}
|
{t("New doc")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -181,14 +173,13 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
</Actions>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { documents, theme, t } = this.props;
|
const { documents, t } = this.props;
|
||||||
|
|
||||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
|
||||||
if (!this.isFetching && !this.collection) return <Search notFound />;
|
if (!this.isFetching && !this.collection) return <Search notFound />;
|
||||||
|
|
||||||
const pinnedDocuments = this.collection
|
const pinnedDocuments = this.collection
|
||||||
@@ -197,181 +188,171 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
const collection = this.collection;
|
const collection = this.collection;
|
||||||
const collectionName = collection ? collection.name : "";
|
const collectionName = collection ? collection.name : "";
|
||||||
const hasPinnedDocuments = !!pinnedDocuments.length;
|
const hasPinnedDocuments = !!pinnedDocuments.length;
|
||||||
const hasDescription = collection ? collection.hasDescription : false;
|
|
||||||
|
|
||||||
return (
|
return collection ? (
|
||||||
<CenteredContent>
|
<Scene
|
||||||
{collection ? (
|
textTitle={collection.name}
|
||||||
|
title={
|
||||||
<>
|
<>
|
||||||
<PageTitle title={collection.name} />
|
<CollectionIcon collection={collection} expanded />
|
||||||
{collection.isEmpty ? (
|
|
||||||
<Centered column>
|
{collection.name}
|
||||||
<HelpText>
|
|
||||||
<Trans
|
|
||||||
defaults="<em>{{ collectionName }}</em> doesn’t contain any
|
|
||||||
documents yet."
|
|
||||||
values={{ collectionName }}
|
|
||||||
components={{ em: <strong /> }}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Trans>Get started by creating a new one!</Trans>
|
|
||||||
</HelpText>
|
|
||||||
<Wrapper>
|
|
||||||
<Link to={newDocumentUrl(collection.id)}>
|
|
||||||
<Button icon={<NewDocumentIcon color={theme.buttonText} />}>
|
|
||||||
{t("Create a document")}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{collection.private && (
|
|
||||||
<Button onClick={this.onPermissions} neutral>
|
|
||||||
{t("Manage members")}…
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
<Modal
|
|
||||||
title={t("Collection members")}
|
|
||||||
onRequestClose={this.handlePermissionsModalClose}
|
|
||||||
isOpen={this.permissionsModalOpen}
|
|
||||||
>
|
|
||||||
<CollectionMembers
|
|
||||||
collection={this.collection}
|
|
||||||
onSubmit={this.handlePermissionsModalClose}
|
|
||||||
onEdit={this.handleEditModalOpen}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<Modal
|
|
||||||
title={t("Edit collection")}
|
|
||||||
onRequestClose={this.handleEditModalClose}
|
|
||||||
isOpen={this.editModalOpen}
|
|
||||||
>
|
|
||||||
<CollectionEdit
|
|
||||||
collection={this.collection}
|
|
||||||
onSubmit={this.handleEditModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</Centered>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Heading>
|
|
||||||
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
|
||||||
{collection.name}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{hasDescription && (
|
|
||||||
<React.Suspense fallback={<p>Loading…</p>}>
|
|
||||||
<Editor
|
|
||||||
id={collection.id}
|
|
||||||
key={collection.description}
|
|
||||||
defaultValue={collection.description}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
</React.Suspense>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasPinnedDocuments && (
|
|
||||||
<>
|
|
||||||
<Subheading>
|
|
||||||
<TinyPinIcon size={18} /> {t("Pinned")}
|
|
||||||
</Subheading>
|
|
||||||
<DocumentList documents={pinnedDocuments} showPin />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<Tab to={collectionUrl(collection.id)} exact>
|
|
||||||
{t("Documents")}
|
|
||||||
</Tab>
|
|
||||||
<Tab to={collectionUrl(collection.id, "updated")} exact>
|
|
||||||
{t("Recently updated")}
|
|
||||||
</Tab>
|
|
||||||
<Tab to={collectionUrl(collection.id, "published")} exact>
|
|
||||||
{t("Recently published")}
|
|
||||||
</Tab>
|
|
||||||
<Tab to={collectionUrl(collection.id, "old")} exact>
|
|
||||||
{t("Least recently updated")}
|
|
||||||
</Tab>
|
|
||||||
<Tab to={collectionUrl(collection.id, "alphabetical")} exact>
|
|
||||||
{t("A–Z")}
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<Route path={collectionUrl(collection.id, "alphabetical")}>
|
|
||||||
<PaginatedDocumentList
|
|
||||||
key="alphabetical"
|
|
||||||
documents={documents.alphabeticalInCollection(
|
|
||||||
collection.id
|
|
||||||
)}
|
|
||||||
fetch={documents.fetchAlphabetical}
|
|
||||||
options={{ collectionId: collection.id }}
|
|
||||||
showPin
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path={collectionUrl(collection.id, "old")}>
|
|
||||||
<PaginatedDocumentList
|
|
||||||
key="old"
|
|
||||||
documents={documents.leastRecentlyUpdatedInCollection(
|
|
||||||
collection.id
|
|
||||||
)}
|
|
||||||
fetch={documents.fetchLeastRecentlyUpdated}
|
|
||||||
options={{ collectionId: collection.id }}
|
|
||||||
showPin
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path={collectionUrl(collection.id, "recent")}>
|
|
||||||
<Redirect to={collectionUrl(collection.id, "published")} />
|
|
||||||
</Route>
|
|
||||||
<Route path={collectionUrl(collection.id, "published")}>
|
|
||||||
<PaginatedDocumentList
|
|
||||||
key="published"
|
|
||||||
documents={documents.recentlyPublishedInCollection(
|
|
||||||
collection.id
|
|
||||||
)}
|
|
||||||
fetch={documents.fetchRecentlyPublished}
|
|
||||||
options={{ collectionId: collection.id }}
|
|
||||||
showPublished
|
|
||||||
showPin
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path={collectionUrl(collection.id, "updated")}>
|
|
||||||
<PaginatedDocumentList
|
|
||||||
key="updated"
|
|
||||||
documents={documents.recentlyUpdatedInCollection(
|
|
||||||
collection.id
|
|
||||||
)}
|
|
||||||
fetch={documents.fetchRecentlyUpdated}
|
|
||||||
options={{ collectionId: collection.id }}
|
|
||||||
showPin
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
<Route path={collectionUrl(collection.id)} exact>
|
|
||||||
<PaginatedDocumentList
|
|
||||||
documents={documents.rootInCollection(collection.id)}
|
|
||||||
fetch={documents.fetchPage}
|
|
||||||
options={{
|
|
||||||
collectionId: collection.id,
|
|
||||||
parentDocumentId: null,
|
|
||||||
sort: collection.sort.field,
|
|
||||||
direction: "ASC",
|
|
||||||
}}
|
|
||||||
showNestedDocuments
|
|
||||||
showPin
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.renderActions()}
|
|
||||||
</>
|
</>
|
||||||
|
}
|
||||||
|
actions={this.renderActions()}
|
||||||
|
>
|
||||||
|
{collection.isEmpty ? (
|
||||||
|
<Centered column>
|
||||||
|
<HelpText>
|
||||||
|
<Trans
|
||||||
|
defaults="<em>{{ collectionName }}</em> doesn’t contain any
|
||||||
|
documents yet."
|
||||||
|
values={{ collectionName }}
|
||||||
|
components={{ em: <strong /> }}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Trans>Get started by creating a new one!</Trans>
|
||||||
|
</HelpText>
|
||||||
|
<Empty>
|
||||||
|
<Link to={newDocumentUrl(collection.id)}>
|
||||||
|
<Button icon={<NewDocumentIcon color="currentColor" />}>
|
||||||
|
{t("Create a document")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{collection.private && (
|
||||||
|
<Button onClick={this.onPermissions} neutral>
|
||||||
|
{t("Manage members")}…
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Empty>
|
||||||
|
<Modal
|
||||||
|
title={t("Collection members")}
|
||||||
|
onRequestClose={this.handlePermissionsModalClose}
|
||||||
|
isOpen={this.permissionsModalOpen}
|
||||||
|
>
|
||||||
|
<CollectionMembers
|
||||||
|
collection={this.collection}
|
||||||
|
onSubmit={this.handlePermissionsModalClose}
|
||||||
|
onEdit={this.handleEditModalOpen}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
title={t("Edit collection")}
|
||||||
|
onRequestClose={this.handleEditModalClose}
|
||||||
|
isOpen={this.editModalOpen}
|
||||||
|
>
|
||||||
|
<CollectionEdit
|
||||||
|
collection={this.collection}
|
||||||
|
onSubmit={this.handleEditModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Centered>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Heading>
|
<Heading>
|
||||||
<Mask height={35} />
|
<CollectionIcon collection={collection} size={40} expanded />{" "}
|
||||||
|
{collection.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
<ListPlaceholder count={5} />
|
<CollectionDescription collection={collection} />
|
||||||
|
|
||||||
|
{hasPinnedDocuments && (
|
||||||
|
<>
|
||||||
|
<Subheading>
|
||||||
|
<TinyPinIcon size={18} /> {t("Pinned")}
|
||||||
|
</Subheading>
|
||||||
|
<DocumentList documents={pinnedDocuments} showPin />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab to={collectionUrl(collection.id)} exact>
|
||||||
|
{t("Documents")}
|
||||||
|
</Tab>
|
||||||
|
<Tab to={collectionUrl(collection.id, "updated")} exact>
|
||||||
|
{t("Recently updated")}
|
||||||
|
</Tab>
|
||||||
|
<Tab to={collectionUrl(collection.id, "published")} exact>
|
||||||
|
{t("Recently published")}
|
||||||
|
</Tab>
|
||||||
|
<Tab to={collectionUrl(collection.id, "old")} exact>
|
||||||
|
{t("Least recently updated")}
|
||||||
|
</Tab>
|
||||||
|
<Tab to={collectionUrl(collection.id, "alphabetical")} exact>
|
||||||
|
{t("A–Z")}
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
<Switch>
|
||||||
|
<Route path={collectionUrl(collection.id, "alphabetical")}>
|
||||||
|
<PaginatedDocumentList
|
||||||
|
key="alphabetical"
|
||||||
|
documents={documents.alphabeticalInCollection(collection.id)}
|
||||||
|
fetch={documents.fetchAlphabetical}
|
||||||
|
options={{ collectionId: collection.id }}
|
||||||
|
showPin
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path={collectionUrl(collection.id, "old")}>
|
||||||
|
<PaginatedDocumentList
|
||||||
|
key="old"
|
||||||
|
documents={documents.leastRecentlyUpdatedInCollection(
|
||||||
|
collection.id
|
||||||
|
)}
|
||||||
|
fetch={documents.fetchLeastRecentlyUpdated}
|
||||||
|
options={{ collectionId: collection.id }}
|
||||||
|
showPin
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path={collectionUrl(collection.id, "recent")}>
|
||||||
|
<Redirect to={collectionUrl(collection.id, "published")} />
|
||||||
|
</Route>
|
||||||
|
<Route path={collectionUrl(collection.id, "published")}>
|
||||||
|
<PaginatedDocumentList
|
||||||
|
key="published"
|
||||||
|
documents={documents.recentlyPublishedInCollection(
|
||||||
|
collection.id
|
||||||
|
)}
|
||||||
|
fetch={documents.fetchRecentlyPublished}
|
||||||
|
options={{ collectionId: collection.id }}
|
||||||
|
showPublished
|
||||||
|
showPin
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path={collectionUrl(collection.id, "updated")}>
|
||||||
|
<PaginatedDocumentList
|
||||||
|
key="updated"
|
||||||
|
documents={documents.recentlyUpdatedInCollection(
|
||||||
|
collection.id
|
||||||
|
)}
|
||||||
|
fetch={documents.fetchRecentlyUpdated}
|
||||||
|
options={{ collectionId: collection.id }}
|
||||||
|
showPin
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
<Route path={collectionUrl(collection.id)} exact>
|
||||||
|
<PaginatedDocumentList
|
||||||
|
documents={documents.rootInCollection(collection.id)}
|
||||||
|
fetch={documents.fetchPage}
|
||||||
|
options={{
|
||||||
|
collectionId: collection.id,
|
||||||
|
parentDocumentId: null,
|
||||||
|
sort: collection.sort.field,
|
||||||
|
direction: "ASC",
|
||||||
|
}}
|
||||||
|
showNestedDocuments
|
||||||
|
showPin
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</Scene>
|
||||||
|
) : (
|
||||||
|
<CenteredContent>
|
||||||
|
<Heading>
|
||||||
|
<Mask height={35} />
|
||||||
|
</Heading>
|
||||||
|
<ListPlaceholder count={5} />
|
||||||
</CenteredContent>
|
</CenteredContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -390,16 +371,11 @@ const TinyPinIcon = styled(PinIcon)`
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled(Flex)`
|
const Empty = styled(Flex)`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default withTranslation()<CollectionScene>(
|
export default withTranslation()<CollectionScene>(
|
||||||
inject(
|
inject("collections", "policies", "documents", "ui")(CollectionScene)
|
||||||
"collections",
|
|
||||||
"policies",
|
|
||||||
"documents",
|
|
||||||
"ui"
|
|
||||||
)(withTheme(CollectionScene))
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import Flex from "components/Flex";
|
|||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import IconPicker from "components/IconPicker";
|
import IconPicker from "components/IconPicker";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
import InputRich from "components/InputRich";
|
|
||||||
import InputSelect from "components/InputSelect";
|
import InputSelect from "components/InputSelect";
|
||||||
import Switch from "components/Switch";
|
import Switch from "components/Switch";
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ type Props = {
|
|||||||
class CollectionEdit extends React.Component<Props> {
|
class CollectionEdit extends React.Component<Props> {
|
||||||
@observable name: string = this.props.collection.name;
|
@observable name: string = this.props.collection.name;
|
||||||
@observable sharing: boolean = this.props.collection.sharing;
|
@observable sharing: boolean = this.props.collection.sharing;
|
||||||
@observable description: string = this.props.collection.description;
|
|
||||||
@observable icon: string = this.props.collection.icon;
|
@observable icon: string = this.props.collection.icon;
|
||||||
@observable color: string = this.props.collection.color || "#4E5C6E";
|
@observable color: string = this.props.collection.color || "#4E5C6E";
|
||||||
@observable private: boolean = this.props.collection.private;
|
@observable private: boolean = this.props.collection.private;
|
||||||
@@ -43,7 +41,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
try {
|
try {
|
||||||
await this.props.collection.save({
|
await this.props.collection.save({
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
private: this.private,
|
private: this.private,
|
||||||
@@ -69,10 +66,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDescriptionChange = (getValue: () => string) => {
|
|
||||||
this.description = getValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||||
this.name = ev.target.value;
|
this.name = ev.target.value;
|
||||||
};
|
};
|
||||||
@@ -120,15 +113,6 @@ class CollectionEdit extends React.Component<Props> {
|
|||||||
icon={this.icon}
|
icon={this.icon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InputRich
|
|
||||||
id={this.props.collection.id}
|
|
||||||
label={t("Description")}
|
|
||||||
onChange={this.handleDescriptionChange}
|
|
||||||
defaultValue={this.description || ""}
|
|
||||||
placeholder={t("More details about this collection…")}
|
|
||||||
minHeight={68}
|
|
||||||
maxHeight={200}
|
|
||||||
/>
|
|
||||||
<InputSelect
|
<InputSelect
|
||||||
label={t("Sort in sidebar")}
|
label={t("Sort in sidebar")}
|
||||||
options={[
|
options={[
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import Flex from "components/Flex";
|
|||||||
import HelpText from "components/HelpText";
|
import HelpText from "components/HelpText";
|
||||||
import IconPicker, { icons } from "components/IconPicker";
|
import IconPicker, { icons } from "components/IconPicker";
|
||||||
import Input from "components/Input";
|
import Input from "components/Input";
|
||||||
import InputRich from "components/InputRich";
|
|
||||||
import Switch from "components/Switch";
|
import Switch from "components/Switch";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -29,7 +28,6 @@ type Props = {
|
|||||||
@observer
|
@observer
|
||||||
class CollectionNew extends React.Component<Props> {
|
class CollectionNew extends React.Component<Props> {
|
||||||
@observable name: string = "";
|
@observable name: string = "";
|
||||||
@observable description: string = "";
|
|
||||||
@observable icon: string = "";
|
@observable icon: string = "";
|
||||||
@observable color: string = "#4E5C6E";
|
@observable color: string = "#4E5C6E";
|
||||||
@observable sharing: boolean = true;
|
@observable sharing: boolean = true;
|
||||||
@@ -43,7 +41,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
const collection = new Collection(
|
const collection = new Collection(
|
||||||
{
|
{
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
|
||||||
sharing: this.sharing,
|
sharing: this.sharing,
|
||||||
icon: this.icon,
|
icon: this.icon,
|
||||||
color: this.color,
|
color: this.color,
|
||||||
@@ -90,10 +87,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
this.hasOpenedIconPicker = true;
|
this.hasOpenedIconPicker = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDescriptionChange = (getValue: () => string) => {
|
|
||||||
this.description = getValue();
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
handlePrivateChange = (ev: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.private = ev.target.checked;
|
this.private = ev.target.checked;
|
||||||
};
|
};
|
||||||
@@ -115,9 +108,9 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<HelpText>
|
<HelpText>
|
||||||
<Trans>
|
<Trans>
|
||||||
Collections are for grouping your knowledge base. They work best
|
Collections are for grouping your documents. They work best when
|
||||||
when organized around a topic or internal team — Product or
|
organized around a topic or internal team — Product or Engineering
|
||||||
Engineering for example.
|
for example.
|
||||||
</Trans>
|
</Trans>
|
||||||
</HelpText>
|
</HelpText>
|
||||||
<Flex>
|
<Flex>
|
||||||
@@ -138,14 +131,6 @@ class CollectionNew extends React.Component<Props> {
|
|||||||
icon={this.icon}
|
icon={this.icon}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InputRich
|
|
||||||
label={t("Description")}
|
|
||||||
onChange={this.handleDescriptionChange}
|
|
||||||
defaultValue={this.description || ""}
|
|
||||||
placeholder={t("More details about this collection…")}
|
|
||||||
minHeight={68}
|
|
||||||
maxHeight={200}
|
|
||||||
/>
|
|
||||||
<Switch
|
<Switch
|
||||||
id="private"
|
id="private"
|
||||||
label={t("Private collection")}
|
label={t("Private collection")}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { observer, inject } from "mobx-react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { RouterHistory, Match } from "react-router-dom";
|
import type { RouterHistory, Match } from "react-router-dom";
|
||||||
import { withRouter } from "react-router-dom";
|
import { withRouter } from "react-router-dom";
|
||||||
import { withTheme } from "styled-components";
|
|
||||||
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
import PoliciesStore from "stores/PoliciesStore";
|
||||||
@@ -22,7 +21,7 @@ import DocumentComponent from "./Document";
|
|||||||
import HideSidebar from "./HideSidebar";
|
import HideSidebar from "./HideSidebar";
|
||||||
import Loading from "./Loading";
|
import Loading from "./Loading";
|
||||||
import SocketPresence from "./SocketPresence";
|
import SocketPresence from "./SocketPresence";
|
||||||
import { type LocationWithState, type Theme } from "types";
|
import { type LocationWithState } from "types";
|
||||||
import { NotFoundError, OfflineError } from "utils/errors";
|
import { NotFoundError, OfflineError } from "utils/errors";
|
||||||
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||||
import { isInternalUrl } from "utils/urls";
|
import { isInternalUrl } from "utils/urls";
|
||||||
@@ -35,7 +34,6 @@ type Props = {|
|
|||||||
policies: PoliciesStore,
|
policies: PoliciesStore,
|
||||||
revisions: RevisionsStore,
|
revisions: RevisionsStore,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
theme: Theme,
|
|
||||||
history: RouterHistory,
|
history: RouterHistory,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
@@ -49,7 +47,6 @@ class DataLoader extends React.Component<Props> {
|
|||||||
const { documents, match } = this.props;
|
const { documents, match } = this.props;
|
||||||
this.document = documents.getByUrl(match.params.documentSlug);
|
this.document = documents.getByUrl(match.params.documentSlug);
|
||||||
this.loadDocument();
|
this.loadDocument();
|
||||||
this.updateBackground();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
@@ -74,13 +71,6 @@ class DataLoader extends React.Component<Props> {
|
|||||||
) {
|
) {
|
||||||
this.loadRevision();
|
this.loadRevision();
|
||||||
}
|
}
|
||||||
this.updateBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBackground() {
|
|
||||||
// ensure the wider page color always matches the theme. This is to
|
|
||||||
// account for share links which don't sit in the wider Layout component
|
|
||||||
window.document.body.style.background = this.props.theme.background;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEditing() {
|
get isEditing() {
|
||||||
@@ -266,5 +256,5 @@ export default withRouter(
|
|||||||
"revisions",
|
"revisions",
|
||||||
"policies",
|
"policies",
|
||||||
"shares"
|
"shares"
|
||||||
)(withTheme(DataLoader))
|
)(DataLoader)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ const ReferencesWrapper = styled("div")`
|
|||||||
const MaxWidth = styled(Flex)`
|
const MaxWidth = styled(Flex)`
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
props.archived && `* { color: ${props.theme.textSecondary} !important; } `};
|
||||||
padding: 0 16px;
|
padding: 0 12px;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { throttle } from "lodash";
|
import { observer } from "mobx-react";
|
||||||
import { observable } from "mobx";
|
|
||||||
import { observer, inject } from "mobx-react";
|
|
||||||
import {
|
import {
|
||||||
TableOfContentsIcon,
|
TableOfContentsIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
@@ -9,18 +7,11 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
MoreIcon,
|
MoreIcon,
|
||||||
} from "outline-icons";
|
} from "outline-icons";
|
||||||
import { transparentize, darken } from "polished";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, Trans, type TFunction } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
|
||||||
import AuthStore from "stores/AuthStore";
|
|
||||||
import PoliciesStore from "stores/PoliciesStore";
|
|
||||||
import SharesStore from "stores/SharesStore";
|
|
||||||
import UiStore from "stores/UiStore";
|
|
||||||
import Document from "models/Document";
|
import Document from "models/Document";
|
||||||
|
|
||||||
import DocumentShare from "scenes/DocumentShare";
|
import DocumentShare from "scenes/DocumentShare";
|
||||||
import { Action, Separator } from "components/Actions";
|
import { Action, Separator } from "components/Actions";
|
||||||
import Badge from "components/Badge";
|
import Badge from "components/Badge";
|
||||||
@@ -28,20 +19,17 @@ import Breadcrumb, { Slash } from "components/Breadcrumb";
|
|||||||
import Button from "components/Button";
|
import Button from "components/Button";
|
||||||
import Collaborators from "components/Collaborators";
|
import Collaborators from "components/Collaborators";
|
||||||
import Fade from "components/Fade";
|
import Fade from "components/Fade";
|
||||||
import Flex from "components/Flex";
|
import Header from "components/Header";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
import Tooltip from "components/Tooltip";
|
import Tooltip from "components/Tooltip";
|
||||||
|
import useStores from "hooks/useStores";
|
||||||
import DocumentMenu from "menus/DocumentMenu";
|
import DocumentMenu from "menus/DocumentMenu";
|
||||||
import NewChildDocumentMenu from "menus/NewChildDocumentMenu";
|
import NewChildDocumentMenu from "menus/NewChildDocumentMenu";
|
||||||
import TemplatesMenu from "menus/TemplatesMenu";
|
import TemplatesMenu from "menus/TemplatesMenu";
|
||||||
import { metaDisplay } from "utils/keyboard";
|
import { metaDisplay } from "utils/keyboard";
|
||||||
import { newDocumentUrl, editDocumentUrl } from "utils/routeHelpers";
|
import { newDocumentUrl, editDocumentUrl } from "utils/routeHelpers";
|
||||||
|
|
||||||
type Props = {
|
type Props = {|
|
||||||
auth: AuthStore,
|
|
||||||
ui: UiStore,
|
|
||||||
shares: SharesStore,
|
|
||||||
policies: PoliciesStore,
|
|
||||||
document: Document,
|
document: Document,
|
||||||
isDraft: boolean,
|
isDraft: boolean,
|
||||||
isEditing: boolean,
|
isEditing: boolean,
|
||||||
@@ -56,356 +44,263 @@ type Props = {
|
|||||||
publish?: boolean,
|
publish?: boolean,
|
||||||
autosave?: boolean,
|
autosave?: boolean,
|
||||||
}) => void,
|
}) => void,
|
||||||
t: TFunction,
|
|};
|
||||||
};
|
|
||||||
|
|
||||||
@observer
|
function DocumentHeader({
|
||||||
class Header extends React.Component<Props> {
|
document,
|
||||||
@observable isScrolled = false;
|
isEditing,
|
||||||
@observable showShareModal = false;
|
isDraft,
|
||||||
|
isPublishing,
|
||||||
|
isRevision,
|
||||||
|
isSaving,
|
||||||
|
savingIsDisabled,
|
||||||
|
publishingIsDisabled,
|
||||||
|
onSave,
|
||||||
|
}: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { auth, ui, shares, policies } = useStores();
|
||||||
|
const [showShareModal, setShowShareModal] = React.useState(false);
|
||||||
|
|
||||||
componentDidMount() {
|
const handleSave = React.useCallback(() => {
|
||||||
window.addEventListener("scroll", this.handleScroll);
|
onSave({ done: true });
|
||||||
}
|
}, [onSave]);
|
||||||
|
|
||||||
componentWillUnmount() {
|
const handlePublish = React.useCallback(() => {
|
||||||
window.removeEventListener("scroll", this.handleScroll);
|
onSave({ done: true, publish: true });
|
||||||
}
|
}, [onSave]);
|
||||||
|
|
||||||
updateIsScrolled = () => {
|
const handleShareLink = React.useCallback(
|
||||||
this.isScrolled = window.scrollY > 75;
|
async (ev: SyntheticEvent<>) => {
|
||||||
};
|
await document.share();
|
||||||
|
|
||||||
handleScroll = throttle(this.updateIsScrolled, 50);
|
setShowShareModal(true);
|
||||||
|
},
|
||||||
|
[document]
|
||||||
|
);
|
||||||
|
|
||||||
handleSave = () => {
|
const handleCloseShareModal = React.useCallback(() => {
|
||||||
this.props.onSave({ done: true });
|
setShowShareModal(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
handlePublish = () => {
|
const share = shares.getByDocumentId(document.id);
|
||||||
this.props.onSave({ done: true, publish: true });
|
const isPubliclyShared = share && share.published;
|
||||||
};
|
const isNew = document.isNew;
|
||||||
|
const isTemplate = document.isTemplate;
|
||||||
|
const can = policies.abilities(document.id);
|
||||||
|
const canShareDocument = auth.team && auth.team.sharing && can.share;
|
||||||
|
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
||||||
|
const canEdit = can.update && !isEditing;
|
||||||
|
|
||||||
handleShareLink = async (ev: SyntheticEvent<>) => {
|
return (
|
||||||
const { document } = this.props;
|
<>
|
||||||
await document.share();
|
<Modal
|
||||||
|
isOpen={showShareModal}
|
||||||
this.showShareModal = true;
|
onRequestClose={handleCloseShareModal}
|
||||||
};
|
title={t("Share document")}
|
||||||
|
|
||||||
handleCloseShareModal = () => {
|
|
||||||
this.showShareModal = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClickTitle = () => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
shares,
|
|
||||||
document,
|
|
||||||
policies,
|
|
||||||
isEditing,
|
|
||||||
isDraft,
|
|
||||||
isPublishing,
|
|
||||||
isRevision,
|
|
||||||
isSaving,
|
|
||||||
savingIsDisabled,
|
|
||||||
publishingIsDisabled,
|
|
||||||
ui,
|
|
||||||
auth,
|
|
||||||
t,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const share = shares.getByDocumentId(document.id);
|
|
||||||
const isPubliclyShared = share && share.published;
|
|
||||||
const isNew = document.isNew;
|
|
||||||
const isTemplate = document.isTemplate;
|
|
||||||
const can = policies.abilities(document.id);
|
|
||||||
const canShareDocument = auth.team && auth.team.sharing && can.share;
|
|
||||||
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
|
|
||||||
const canEdit = can.update && !isEditing;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Actions
|
|
||||||
align="center"
|
|
||||||
justify="space-between"
|
|
||||||
isCompact={this.isScrolled}
|
|
||||||
shrink={false}
|
|
||||||
>
|
>
|
||||||
<Modal
|
<DocumentShare document={document} onSubmit={handleCloseShareModal} />
|
||||||
isOpen={this.showShareModal}
|
</Modal>
|
||||||
onRequestClose={this.handleCloseShareModal}
|
<Header
|
||||||
title={t("Share document")}
|
breadcrumb={
|
||||||
>
|
<Breadcrumb document={document}>
|
||||||
<DocumentShare
|
{!isEditing && (
|
||||||
document={document}
|
<>
|
||||||
onSubmit={this.handleCloseShareModal}
|
<Slash />
|
||||||
/>
|
<Tooltip
|
||||||
</Modal>
|
tooltip={
|
||||||
<Breadcrumb document={document}>
|
ui.tocVisible ? t("Hide contents") : t("Show contents")
|
||||||
{!isEditing && (
|
|
||||||
<>
|
|
||||||
<Slash />
|
|
||||||
<Tooltip
|
|
||||||
tooltip={
|
|
||||||
ui.tocVisible ? t("Hide contents") : t("Show contents")
|
|
||||||
}
|
|
||||||
shortcut={`ctrl+${metaDisplay}+h`}
|
|
||||||
delay={250}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={
|
|
||||||
ui.tocVisible
|
|
||||||
? ui.hideTableOfContents
|
|
||||||
: ui.showTableOfContents
|
|
||||||
}
|
}
|
||||||
icon={<TableOfContentsIcon />}
|
shortcut={`ctrl+${metaDisplay}+h`}
|
||||||
iconColor="currentColor"
|
delay={250}
|
||||||
borderOnHover
|
placement="bottom"
|
||||||
neutral
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Breadcrumb>
|
|
||||||
{this.isScrolled && (
|
|
||||||
<Title onClick={this.handleClickTitle}>
|
|
||||||
<Fade>
|
|
||||||
{document.title}{" "}
|
|
||||||
{document.isArchived && <Badge>{t("Archived")}</Badge>}
|
|
||||||
</Fade>
|
|
||||||
</Title>
|
|
||||||
)}
|
|
||||||
<Wrapper align="center" justify="flex-end">
|
|
||||||
{isSaving && !isPublishing && (
|
|
||||||
<Action>
|
|
||||||
<Status>{t("Saving")}…</Status>
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Fade>
|
|
||||||
<Collaborators
|
|
||||||
document={document}
|
|
||||||
currentUserId={auth.user ? auth.user.id : undefined}
|
|
||||||
/>
|
|
||||||
</Fade>
|
|
||||||
{isEditing && !isTemplate && isNew && (
|
|
||||||
<Action>
|
|
||||||
<TemplatesMenu document={document} />
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
{!isEditing && canShareDocument && (
|
|
||||||
<Action>
|
|
||||||
<Tooltip
|
|
||||||
tooltip={
|
|
||||||
isPubliclyShared ? (
|
|
||||||
<Trans>
|
|
||||||
Anyone with the link <br />
|
|
||||||
can view this document
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delay={500}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon={isPubliclyShared ? <GlobeIcon /> : undefined}
|
|
||||||
onClick={this.handleShareLink}
|
|
||||||
neutral
|
|
||||||
>
|
>
|
||||||
{t("Share")}
|
<Button
|
||||||
</Button>
|
onClick={
|
||||||
</Tooltip>
|
ui.tocVisible
|
||||||
</Action>
|
? ui.hideTableOfContents
|
||||||
)}
|
: ui.showTableOfContents
|
||||||
{isEditing && (
|
}
|
||||||
<>
|
icon={<TableOfContentsIcon />}
|
||||||
|
iconColor="currentColor"
|
||||||
|
borderOnHover
|
||||||
|
neutral
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Breadcrumb>
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{document.title}{" "}
|
||||||
|
{document.isArchived && <Badge>{t("Archived")}</Badge>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{isSaving && !isPublishing && (
|
||||||
|
<Action>
|
||||||
|
<Status>{t("Saving")}…</Status>
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Fade>
|
||||||
|
<Collaborators
|
||||||
|
document={document}
|
||||||
|
currentUserId={auth.user ? auth.user.id : undefined}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
{isEditing && !isTemplate && isNew && (
|
||||||
|
<Action>
|
||||||
|
<TemplatesMenu document={document} />
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
{!isEditing && canShareDocument && (
|
||||||
<Action>
|
<Action>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltip={t("Save")}
|
tooltip={
|
||||||
shortcut={`${metaDisplay}+enter`}
|
isPubliclyShared ? (
|
||||||
|
<Trans>
|
||||||
|
Anyone with the link <br />
|
||||||
|
can view this document
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
delay={500}
|
delay={500}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={this.handleSave}
|
icon={isPubliclyShared ? <GlobeIcon /> : undefined}
|
||||||
disabled={savingIsDisabled}
|
onClick={handleShareLink}
|
||||||
neutral={isDraft}
|
neutral
|
||||||
>
|
>
|
||||||
{isDraft ? t("Save Draft") : t("Done Editing")}
|
{t("Share")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Action>
|
</Action>
|
||||||
</>
|
)}
|
||||||
)}
|
{isEditing && (
|
||||||
{canEdit && (
|
<>
|
||||||
<Action>
|
<Action>
|
||||||
<Tooltip
|
|
||||||
tooltip={t("Edit {{noun}}", { noun: document.noun })}
|
|
||||||
shortcut="e"
|
|
||||||
delay={500}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
as={Link}
|
|
||||||
icon={<EditIcon />}
|
|
||||||
to={editDocumentUrl(this.props.document)}
|
|
||||||
neutral
|
|
||||||
>
|
|
||||||
{t("Edit")}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
{canEdit && can.createChildDocument && (
|
|
||||||
<Action>
|
|
||||||
<NewChildDocumentMenu
|
|
||||||
document={document}
|
|
||||||
label={(props) => (
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
tooltip={t("New document")}
|
tooltip={t("Save")}
|
||||||
shortcut="n"
|
shortcut={`${metaDisplay}+enter`}
|
||||||
delay={500}
|
delay={500}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<Button icon={<PlusIcon />} {...props} neutral>
|
<Button
|
||||||
{t("New doc")}
|
onClick={handleSave}
|
||||||
|
disabled={savingIsDisabled}
|
||||||
|
neutral={isDraft}
|
||||||
|
>
|
||||||
|
{isDraft ? t("Save Draft") : t("Done Editing")}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
</Action>
|
||||||
/>
|
</>
|
||||||
</Action>
|
)}
|
||||||
)}
|
{canEdit && (
|
||||||
{canEdit && isTemplate && !isDraft && !isRevision && (
|
|
||||||
<Action>
|
|
||||||
<Button
|
|
||||||
icon={<PlusIcon />}
|
|
||||||
as={Link}
|
|
||||||
to={newDocumentUrl(document.collectionId, {
|
|
||||||
templateId: document.id,
|
|
||||||
})}
|
|
||||||
primary
|
|
||||||
>
|
|
||||||
{t("New from template")}
|
|
||||||
</Button>
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
{can.update && isDraft && !isRevision && (
|
|
||||||
<Action>
|
|
||||||
<Tooltip
|
|
||||||
tooltip={t("Publish")}
|
|
||||||
shortcut={`${metaDisplay}+shift+p`}
|
|
||||||
delay={500}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={this.handlePublish}
|
|
||||||
disabled={publishingIsDisabled}
|
|
||||||
>
|
|
||||||
{isPublishing ? `${t("Publishing")}…` : t("Publish")}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Action>
|
|
||||||
)}
|
|
||||||
{!isEditing && (
|
|
||||||
<>
|
|
||||||
<Separator />
|
|
||||||
<Action>
|
<Action>
|
||||||
<DocumentMenu
|
<Tooltip
|
||||||
|
tooltip={t("Edit {{noun}}", { noun: document.noun })}
|
||||||
|
shortcut="e"
|
||||||
|
delay={500}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
icon={<EditIcon />}
|
||||||
|
to={editDocumentUrl(document)}
|
||||||
|
neutral
|
||||||
|
>
|
||||||
|
{t("Edit")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
{canEdit && can.createChildDocument && (
|
||||||
|
<Action>
|
||||||
|
<NewChildDocumentMenu
|
||||||
document={document}
|
document={document}
|
||||||
isRevision={isRevision}
|
|
||||||
label={(props) => (
|
label={(props) => (
|
||||||
<Button
|
<Tooltip
|
||||||
icon={<MoreIcon />}
|
tooltip={t("New document")}
|
||||||
iconColor="currentColor"
|
shortcut="n"
|
||||||
{...props}
|
delay={500}
|
||||||
borderOnHover
|
placement="bottom"
|
||||||
neutral
|
>
|
||||||
/>
|
<Button icon={<PlusIcon />} {...props} neutral>
|
||||||
|
{t("New doc")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
showToggleEmbeds={canToggleEmbeds}
|
|
||||||
showPrint
|
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
</>
|
)}
|
||||||
)}
|
{canEdit && isTemplate && !isDraft && !isRevision && (
|
||||||
</Wrapper>
|
<Action>
|
||||||
</Actions>
|
<Button
|
||||||
);
|
icon={<PlusIcon />}
|
||||||
}
|
as={Link}
|
||||||
|
to={newDocumentUrl(document.collectionId, {
|
||||||
|
templateId: document.id,
|
||||||
|
})}
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
{t("New from template")}
|
||||||
|
</Button>
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
{can.update && isDraft && !isRevision && (
|
||||||
|
<Action>
|
||||||
|
<Tooltip
|
||||||
|
tooltip={t("Publish")}
|
||||||
|
shortcut={`${metaDisplay}+shift+p`}
|
||||||
|
delay={500}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={publishingIsDisabled}
|
||||||
|
>
|
||||||
|
{isPublishing ? `${t("Publishing")}…` : t("Publish")}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
{!isEditing && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<Action>
|
||||||
|
<DocumentMenu
|
||||||
|
document={document}
|
||||||
|
isRevision={isRevision}
|
||||||
|
label={(props) => (
|
||||||
|
<Button
|
||||||
|
icon={<MoreIcon />}
|
||||||
|
iconColor="currentColor"
|
||||||
|
{...props}
|
||||||
|
borderOnHover
|
||||||
|
neutral
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
showToggleEmbeds={canToggleEmbeds}
|
||||||
|
showPrint
|
||||||
|
/>
|
||||||
|
</Action>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status = styled.div`
|
const Status = styled.div`
|
||||||
color: ${(props) => props.theme.slate};
|
color: ${(props) => props.theme.slate};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled(Flex)`
|
export default observer(DocumentHeader);
|
||||||
width: 100%;
|
|
||||||
align-self: flex-end;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
width: 33.3%;
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Actions = styled(Flex)`
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background: ${(props) => transparentize(0.2, props.theme.background)};
|
|
||||||
box-shadow: 0 1px 0
|
|
||||||
${(props) =>
|
|
||||||
props.isCompact
|
|
||||||
? darken(0.05, props.theme.sidebarBackground)
|
|
||||||
: "transparent"};
|
|
||||||
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`)};
|
|
||||||
|
|
||||||
> div {
|
|
||||||
width: 33.3%;
|
|
||||||
}
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled.div`
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
${breakpoint("tablet")`
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default withTranslation()<Header>(
|
|
||||||
inject("auth", "ui", "policies", "shares")(Header)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { inject, observer } from "mobx-react";
|
import { inject, observer } from "mobx-react";
|
||||||
|
import { EditIcon } from "outline-icons";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { withTranslation, type TFunction } from "react-i18next";
|
import { withTranslation, type TFunction } from "react-i18next";
|
||||||
@@ -9,15 +10,13 @@ import styled from "styled-components";
|
|||||||
import DocumentsStore from "stores/DocumentsStore";
|
import DocumentsStore from "stores/DocumentsStore";
|
||||||
import CollectionFilter from "scenes/Search/components/CollectionFilter";
|
import CollectionFilter from "scenes/Search/components/CollectionFilter";
|
||||||
import DateFilter from "scenes/Search/components/DateFilter";
|
import DateFilter from "scenes/Search/components/DateFilter";
|
||||||
|
import { Action } from "components/Actions";
|
||||||
import Actions, { Action } from "components/Actions";
|
|
||||||
import CenteredContent from "components/CenteredContent";
|
|
||||||
import Empty from "components/Empty";
|
import Empty from "components/Empty";
|
||||||
import Flex from "components/Flex";
|
import Flex from "components/Flex";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import InputSearch from "components/InputSearch";
|
import InputSearch from "components/InputSearch";
|
||||||
import PageTitle from "components/PageTitle";
|
|
||||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||||
|
import Scene from "components/Scene";
|
||||||
import Subheading from "components/Subheading";
|
import Subheading from "components/Subheading";
|
||||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||||
import { type LocationWithState } from "types";
|
import { type LocationWithState } from "types";
|
||||||
@@ -78,8 +77,24 @@ class Drafts extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<Scene
|
||||||
<PageTitle title={t("Drafts")} />
|
icon={<EditIcon color="currentColor" />}
|
||||||
|
title={t("Drafts")}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Action>
|
||||||
|
<InputSearch
|
||||||
|
source="drafts"
|
||||||
|
label={t("Search documents")}
|
||||||
|
labelHidden
|
||||||
|
/>
|
||||||
|
</Action>
|
||||||
|
<Action>
|
||||||
|
<NewDocumentMenu />
|
||||||
|
</Action>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Heading>{t("Drafts")}</Heading>
|
<Heading>{t("Drafts")}</Heading>
|
||||||
<Subheading>
|
<Subheading>
|
||||||
{t("Documents")}
|
{t("Documents")}
|
||||||
@@ -110,20 +125,7 @@ class Drafts extends React.Component<Props> {
|
|||||||
options={options}
|
options={options}
|
||||||
showCollection
|
showCollection
|
||||||
/>
|
/>
|
||||||
|
</Scene>
|
||||||
<Actions align="center" justify="flex-end">
|
|
||||||
<Action>
|
|
||||||
<InputSearch
|
|
||||||
source="drafts"
|
|
||||||
label={t("Search documents")}
|
|
||||||
labelHidden
|
|
||||||
/>
|
|
||||||
</Action>
|
|
||||||
<Action>
|
|
||||||
<NewDocumentMenu />
|
|
||||||
</Action>
|
|
||||||
</Actions>
|
|
||||||
</CenteredContent>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { HomeIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom";
|
||||||
|
import { Action } from "components/Actions";
|
||||||
import Actions, { Action } from "components/Actions";
|
import Heading from "components/Heading";
|
||||||
import CenteredContent from "components/CenteredContent";
|
|
||||||
import InputSearch from "components/InputSearch";
|
import InputSearch from "components/InputSearch";
|
||||||
import LanguagePrompt from "components/LanguagePrompt";
|
import LanguagePrompt from "components/LanguagePrompt";
|
||||||
import PageTitle from "components/PageTitle";
|
import Scene from "components/Scene";
|
||||||
import Tab from "components/Tab";
|
import Tab from "components/Tab";
|
||||||
import Tabs from "components/Tabs";
|
import Tabs from "components/Tabs";
|
||||||
import PaginatedDocumentList from "../components/PaginatedDocumentList";
|
import PaginatedDocumentList from "../components/PaginatedDocumentList";
|
||||||
import useStores from "../hooks/useStores";
|
import useStores from "../hooks/useStores";
|
||||||
import NewDocumentMenu from "menus/NewDocumentMenu";
|
import NewDocumentMenu from "menus/NewDocumentMenu";
|
||||||
|
|
||||||
function Dashboard() {
|
function Home() {
|
||||||
const { documents, ui, auth } = useStores();
|
const { documents, ui, auth } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -23,10 +23,26 @@ function Dashboard() {
|
|||||||
const user = auth.user.id;
|
const user = auth.user.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent>
|
<Scene
|
||||||
<PageTitle title={t("Home")} />
|
icon={<HomeIcon color="currentColor" />}
|
||||||
|
title={t("Home")}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Action>
|
||||||
|
<InputSearch
|
||||||
|
source="dashboard"
|
||||||
|
label={t("Search documents")}
|
||||||
|
labelHidden
|
||||||
|
/>
|
||||||
|
</Action>
|
||||||
|
<Action>
|
||||||
|
<NewDocumentMenu />
|
||||||
|
</Action>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
{!ui.languagePromptDismissed && <LanguagePrompt />}
|
||||||
<h1>{t("Home")}</h1>
|
<Heading>{t("Home")}</Heading>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab to="/home" exact>
|
<Tab to="/home" exact>
|
||||||
{t("Recently updated")}
|
{t("Recently updated")}
|
||||||
@@ -62,20 +78,8 @@ function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
<Actions align="center" justify="flex-end">
|
</Scene>
|
||||||
<Action>
|
|
||||||
<InputSearch
|
|
||||||
source="dashboard"
|
|
||||||
label={t("Search documents")}
|
|
||||||
labelHidden
|
|
||||||
/>
|
|
||||||
</Action>
|
|
||||||
<Action>
|
|
||||||
<NewDocumentMenu />
|
|
||||||
</Action>
|
|
||||||
</Actions>
|
|
||||||
</CenteredContent>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(Dashboard);
|
export default observer(Home);
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { StarredIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type Match } from "react-router-dom";
|
import { type Match } from "react-router-dom";
|
||||||
import Actions, { Action } from "components/Actions";
|
import { Action } from "components/Actions";
|
||||||
import CenteredContent from "components/CenteredContent";
|
|
||||||
import Empty from "components/Empty";
|
import Empty from "components/Empty";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import InputSearch from "components/InputSearch";
|
import InputSearch from "components/InputSearch";
|
||||||
import PageTitle from "components/PageTitle";
|
|
||||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||||
|
import Scene from "components/Scene";
|
||||||
import Tab from "components/Tab";
|
import Tab from "components/Tab";
|
||||||
import Tabs from "components/Tabs";
|
import Tabs from "components/Tabs";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
@@ -26,8 +26,24 @@ function Starred(props: Props) {
|
|||||||
const { sort } = props.match.params;
|
const { sort } = props.match.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<Scene
|
||||||
<PageTitle title={t("Starred")} />
|
icon={<StarredIcon color="currentColor" />}
|
||||||
|
title={t("Starred")}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Action>
|
||||||
|
<InputSearch
|
||||||
|
source="starred"
|
||||||
|
label={t("Search documents")}
|
||||||
|
labelHidden
|
||||||
|
/>
|
||||||
|
</Action>
|
||||||
|
<Action>
|
||||||
|
<NewDocumentMenu />
|
||||||
|
</Action>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Heading>{t("Starred")}</Heading>
|
<Heading>{t("Starred")}</Heading>
|
||||||
<PaginatedDocumentList
|
<PaginatedDocumentList
|
||||||
heading={
|
heading={
|
||||||
@@ -45,20 +61,7 @@ function Starred(props: Props) {
|
|||||||
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
documents={sort === "alphabetical" ? starredAlphabetical : starred}
|
||||||
showCollection
|
showCollection
|
||||||
/>
|
/>
|
||||||
|
</Scene>
|
||||||
<Actions align="center" justify="flex-end">
|
|
||||||
<Action>
|
|
||||||
<InputSearch
|
|
||||||
source="starred"
|
|
||||||
label={t("Search documents")}
|
|
||||||
labelHidden
|
|
||||||
/>
|
|
||||||
</Action>
|
|
||||||
<Action>
|
|
||||||
<NewDocumentMenu />
|
|
||||||
</Action>
|
|
||||||
</Actions>
|
|
||||||
</CenteredContent>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { TemplateIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { type Match } from "react-router-dom";
|
import { type Match } from "react-router-dom";
|
||||||
|
import { Action } from "components/Actions";
|
||||||
import Actions, { Action } from "components/Actions";
|
|
||||||
import CenteredContent from "components/CenteredContent";
|
|
||||||
import Empty from "components/Empty";
|
import Empty from "components/Empty";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import PageTitle from "components/PageTitle";
|
|
||||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||||
|
import Scene from "components/Scene";
|
||||||
import Tab from "components/Tab";
|
import Tab from "components/Tab";
|
||||||
import Tabs from "components/Tabs";
|
import Tabs from "components/Tabs";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
@@ -26,8 +25,15 @@ function Templates(props: Props) {
|
|||||||
const { sort } = props.match.params;
|
const { sort } = props.match.params;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<Scene
|
||||||
<PageTitle title={t("Templates")} />
|
icon={<TemplateIcon color="currentColor" />}
|
||||||
|
title={t("Templates")}
|
||||||
|
actions={
|
||||||
|
<Action>
|
||||||
|
<NewTemplateMenu />
|
||||||
|
</Action>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Heading>{t("Templates")}</Heading>
|
<Heading>{t("Templates")}</Heading>
|
||||||
<PaginatedDocumentList
|
<PaginatedDocumentList
|
||||||
heading={
|
heading={
|
||||||
@@ -52,13 +58,7 @@ function Templates(props: Props) {
|
|||||||
showCollection
|
showCollection
|
||||||
showDraft
|
showDraft
|
||||||
/>
|
/>
|
||||||
|
</Scene>
|
||||||
<Actions align="center" justify="flex-end">
|
|
||||||
<Action>
|
|
||||||
<NewTemplateMenu />
|
|
||||||
</Action>
|
|
||||||
</Actions>
|
|
||||||
</CenteredContent>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { TrashIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import CenteredContent from "components/CenteredContent";
|
|
||||||
import Empty from "components/Empty";
|
import Empty from "components/Empty";
|
||||||
import Heading from "components/Heading";
|
import Heading from "components/Heading";
|
||||||
import PageTitle from "components/PageTitle";
|
|
||||||
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
import PaginatedDocumentList from "components/PaginatedDocumentList";
|
||||||
|
import Scene from "components/Scene";
|
||||||
import Subheading from "components/Subheading";
|
import Subheading from "components/Subheading";
|
||||||
import useStores from "hooks/useStores";
|
import useStores from "hooks/useStores";
|
||||||
|
|
||||||
@@ -16,8 +15,7 @@ function Trash() {
|
|||||||
const { documents } = useStores();
|
const { documents } = useStores();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent column auto>
|
<Scene icon={<TrashIcon color="currentColor" />} title={t("Trash")}>
|
||||||
<PageTitle title={t("Trash")} />
|
|
||||||
<Heading>{t("Trash")}</Heading>
|
<Heading>{t("Trash")}</Heading>
|
||||||
<PaginatedDocumentList
|
<PaginatedDocumentList
|
||||||
documents={documents.deleted}
|
documents={documents.deleted}
|
||||||
@@ -27,7 +25,7 @@ function Trash() {
|
|||||||
showCollection
|
showCollection
|
||||||
showTemplate
|
showTemplate
|
||||||
/>
|
/>
|
||||||
</CenteredContent>
|
</Scene>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ function UserProfile(props: Props) {
|
|||||||
time: distanceInWordsToNow(new Date(user.createdAt)),
|
time: distanceInWordsToNow(new Date(user.createdAt)),
|
||||||
})}
|
})}
|
||||||
{user.isAdmin && (
|
{user.isAdmin && (
|
||||||
<StyledBadge admin={user.isAdmin}>{t("Admin")}</StyledBadge>
|
<StyledBadge primary={user.isAdmin}>{t("Admin")}</StyledBadge>
|
||||||
)}
|
)}
|
||||||
{user.isSuspended && <Badge>{t("Suspended")}</Badge>}
|
{user.isSuspended && <StyledBadge>{t("Suspended")}</StyledBadge>}
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<Edit>
|
<Edit>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import env from "env";
|
|||||||
export function initSentry(history: RouterHistory) {
|
export function initSentry(history: RouterHistory) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: env.SENTRY_DSN,
|
dsn: env.SENTRY_DSN,
|
||||||
|
environment: env.ENVIRONMENT,
|
||||||
|
release: env.RELEASE,
|
||||||
integrations: [
|
integrations: [
|
||||||
new Integrations.BrowserTracing({
|
new Integrations.BrowserTracing({
|
||||||
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
routingInstrumentation: Sentry.reactRouterV5Instrumentation(history),
|
||||||
@@ -14,6 +16,7 @@ export function initSentry(history: RouterHistory) {
|
|||||||
],
|
],
|
||||||
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1,
|
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
|
"ResizeObserver loop completed with undelivered notifications",
|
||||||
"ResizeObserver loop limit exceeded",
|
"ResizeObserver loop limit exceeded",
|
||||||
"AuthorizationError",
|
"AuthorizationError",
|
||||||
"BadRequestError",
|
"BadRequestError",
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -67,9 +67,9 @@
|
|||||||
"@babel/preset-flow": "^7.10.4",
|
"@babel/preset-flow": "^7.10.4",
|
||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@rehooks/window-scroll-position": "^1.0.1",
|
"@rehooks/window-scroll-position": "^1.0.1",
|
||||||
"@sentry/node": "^5.23.0",
|
"@sentry/node": "^6.1.0",
|
||||||
"@sentry/react": "^6.0.1",
|
"@sentry/react": "^6.1.0",
|
||||||
"@sentry/tracing": "^6.0.1",
|
"@sentry/tracing": "^6.1.0",
|
||||||
"@tippy.js/react": "^2.2.2",
|
"@tippy.js/react": "^2.2.2",
|
||||||
"@tommoor/remove-markdown": "0.3.1",
|
"@tommoor/remove-markdown": "0.3.1",
|
||||||
"autotrack": "^2.4.1",
|
"autotrack": "^2.4.1",
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
"copy-to-clipboard": "^3.0.6",
|
"copy-to-clipboard": "^3.0.6",
|
||||||
"core-js": "2",
|
"core-js": "2",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "1.29.0",
|
||||||
|
"dd-trace": "^0.30.6",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"emoji-regex": "^6.5.1",
|
"emoji-regex": "^6.5.1",
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
"react-waypoint": "^9.0.2",
|
"react-waypoint": "^9.0.2",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"reakit": "^1.3.4",
|
"reakit": "^1.3.4",
|
||||||
"rich-markdown-editor": "^11.2.0-0",
|
"rich-markdown-editor": "^11.3.0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"sequelize": "^6.3.4",
|
"sequelize": "^6.3.4",
|
||||||
"sequelize-cli": "^6.2.0",
|
"sequelize-cli": "^6.2.0",
|
||||||
@@ -182,6 +183,7 @@
|
|||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.2.2",
|
"babel-jest": "^26.2.2",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
|
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||||
"eslint": "^7.6.0",
|
"eslint": "^7.6.0",
|
||||||
"eslint-config-react-app": "3.0.6",
|
"eslint-config-react-app": "3.0.6",
|
||||||
"eslint-plugin-flowtype": "^5.2.0",
|
"eslint-plugin-flowtype": "^5.2.0",
|
||||||
@@ -204,11 +206,13 @@
|
|||||||
"url-loader": "^0.6.2",
|
"url-loader": "^0.6.2",
|
||||||
"webpack": "4.44.1",
|
"webpack": "4.44.1",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-manifest-plugin": "^3.0.0"
|
"webpack-manifest-plugin": "^3.0.0",
|
||||||
|
"webpack-pwa-manifest": "^4.3.0",
|
||||||
|
"workbox-webpack-plugin": "^6.1.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"dot-prop": "^5.2.0",
|
"dot-prop": "^5.2.0",
|
||||||
"js-yaml": "^3.13.1"
|
"js-yaml": "^3.13.1"
|
||||||
},
|
},
|
||||||
"version": "0.52.0"
|
"version": "0.53.1"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
BIN
public/icon-256.png
Normal file
BIN
public/icon-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 48 KiB |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "Outline",
|
|
||||||
"name": "Outline",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "/icon-192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icon-512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "/home?source=pwa",
|
|
||||||
"background_color": "#FFFFFF",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#FFFFFF"
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,15 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"transform-class-properties"
|
"transform-class-properties",
|
||||||
|
[
|
||||||
|
"transform-inline-environment-variables",
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"SOURCE_COMMIT",
|
||||||
|
"SOURCE_VERSION"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -114,10 +114,11 @@ if (isProduction) {
|
|||||||
// catch errors in one place, automatically set status and response headers
|
// catch errors in one place, automatically set status and response headers
|
||||||
onerror(app);
|
onerror(app);
|
||||||
|
|
||||||
if (process.env.SENTRY_DSN) {
|
if (env.SENTRY_DSN) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.SENTRY_DSN,
|
dsn: env.SENTRY_DSN,
|
||||||
environment: process.env.NODE_ENV,
|
environment: env.ENVIRONMENT,
|
||||||
|
release: env.RELEASE,
|
||||||
maxBreadcrumbs: 0,
|
maxBreadcrumbs: 0,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
// emitted by Koa when bots attempt to snoop on paths such as wp-admin
|
// emitted by Koa when bots attempt to snoop on paths such as wp-admin
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
// Note: This entire object is stringified in the HTML exposed to the client
|
||||||
|
// do not add anything here that should be a secret or password
|
||||||
export default {
|
export default {
|
||||||
URL: process.env.URL,
|
URL: process.env.URL,
|
||||||
CDN_URL: process.env.CDN_URL || "",
|
CDN_URL: process.env.CDN_URL || "",
|
||||||
DEPLOYMENT: process.env.DEPLOYMENT,
|
DEPLOYMENT: process.env.DEPLOYMENT,
|
||||||
|
ENVIRONMENT: process.env.NODE_ENV,
|
||||||
SENTRY_DSN: process.env.SENTRY_DSN,
|
SENTRY_DSN: process.env.SENTRY_DSN,
|
||||||
TEAM_LOGO: process.env.TEAM_LOGO,
|
TEAM_LOGO: process.env.TEAM_LOGO,
|
||||||
SLACK_KEY: process.env.SLACK_KEY,
|
SLACK_KEY: process.env.SLACK_KEY,
|
||||||
SLACK_APP_ID: process.env.SLACK_APP_ID,
|
SLACK_APP_ID: process.env.SLACK_APP_ID,
|
||||||
SUBDOMAINS_ENABLED: process.env.SUBDOMAINS_ENABLED === "true",
|
SUBDOMAINS_ENABLED: process.env.SUBDOMAINS_ENABLED === "true",
|
||||||
GOOGLE_ANALYTICS_ID: process.env.GOOGLE_ANALYTICS_ID,
|
GOOGLE_ANALYTICS_ID: process.env.GOOGLE_ANALYTICS_ID,
|
||||||
|
RELEASE: process.env.SOURCE_COMMIT || process.env.SOURCE_VERSION || undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
// @flow
|
// @flow
|
||||||
require("dotenv").config({ silent: true });
|
require("dotenv").config({ silent: true });
|
||||||
|
|
||||||
|
// If the DataDog agent is installed and the DD_API_KEY environment variable is
|
||||||
|
// in the environment then we can safely attempt to start the DD tracer
|
||||||
|
if (process.env.DD_API_KEY) {
|
||||||
|
require("dd-trace").init({
|
||||||
|
// SOURCE_COMMIT is used by Docker Hub
|
||||||
|
// SOURCE_VERSION is used by Heroku
|
||||||
|
version: process.env.SOURCE_COMMIT || process.env.SOURCE_VERSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!process.env.SECRET_KEY ||
|
!process.env.SECRET_KEY ||
|
||||||
process.env.SECRET_KEY === "generate_a_new_key"
|
process.env.SECRET_KEY === "generate_a_new_key"
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ router.get("/_health", (ctx) => (ctx.body = "OK"));
|
|||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
router.get("/static/*", async (ctx) => {
|
router.get("/static/*", async (ctx) => {
|
||||||
ctx.set({
|
ctx.set({
|
||||||
|
"Service-Worker-Allowed": "/",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Cache-Control": `max-age=${356 * 24 * 60 * 60}`,
|
"Cache-Control": `max-age=${356 * 24 * 60 * 60}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Outline</title>
|
<title>Outline</title>
|
||||||
|
<meta name="theme-color" content="#FFF" />
|
||||||
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<meta name="description" content="A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…">
|
<meta name="description" content="A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…">
|
||||||
//inject-prefetch//
|
//inject-prefetch//
|
||||||
<link
|
<link
|
||||||
@@ -12,7 +15,12 @@
|
|||||||
href="/favicon-32.png"
|
href="/favicon-32.png"
|
||||||
sizes="32x32"
|
sizes="32x32"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
type="image/png"
|
||||||
|
href="/apple-touch-icon.png"
|
||||||
|
sizes="192x192"
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel="search"
|
rel="search"
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
@@ -46,7 +54,9 @@
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
if (window.localStorage && window.localStorage.getItem("theme") === "dark") {
|
if (window.localStorage && window.localStorage.getItem("theme") === "dark") {
|
||||||
window.document.querySelector("#root").style.background = "#111319";
|
var color = "#111319";
|
||||||
|
document.querySelector("#root").style.background = color;
|
||||||
|
document.querySelector('meta[name="theme-color"]').setAttribute("content", color);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ const s3 = new AWS.S3({
|
|||||||
accessKeyId: AWS_ACCESS_KEY_ID,
|
accessKeyId: AWS_ACCESS_KEY_ID,
|
||||||
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
||||||
region: AWS_REGION,
|
region: AWS_REGION,
|
||||||
|
endpoint: process.env.AWS_S3_UPLOAD_BUCKET_URL.includes(
|
||||||
|
AWS_S3_UPLOAD_BUCKET_NAME
|
||||||
|
)
|
||||||
|
? undefined
|
||||||
|
: new AWS.Endpoint(process.env.AWS_S3_UPLOAD_BUCKET_URL),
|
||||||
signatureVersion: "v4",
|
signatureVersion: "v4",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,7 +115,6 @@ export const uploadToS3FromBuffer = async (
|
|||||||
Key: key,
|
Key: key,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
ContentLength: buffer.length,
|
ContentLength: buffer.length,
|
||||||
ServerSideEncryption: "AES256",
|
|
||||||
Body: buffer,
|
Body: buffer,
|
||||||
})
|
})
|
||||||
.promise();
|
.promise();
|
||||||
@@ -135,7 +139,6 @@ export const uploadToS3FromUrl = async (
|
|||||||
Key: key,
|
Key: key,
|
||||||
ContentType: res.headers["content-type"],
|
ContentType: res.headers["content-type"],
|
||||||
ContentLength: res.headers["content-length"],
|
ContentLength: res.headers["content-length"],
|
||||||
ServerSideEncryption: "AES256",
|
|
||||||
Body: buffer,
|
Body: buffer,
|
||||||
})
|
})
|
||||||
.promise();
|
.promise();
|
||||||
|
|||||||
@@ -8,6 +8,10 @@
|
|||||||
"Drafts": "Drafts",
|
"Drafts": "Drafts",
|
||||||
"Templates": "Templates",
|
"Templates": "Templates",
|
||||||
"Deleted Collection": "Deleted Collection",
|
"Deleted Collection": "Deleted Collection",
|
||||||
|
"Sorry, an error occurred saving the collection": "Sorry, an error occurred saving the collection",
|
||||||
|
"Add a description": "Add a description",
|
||||||
|
"Collapse": "Collapse",
|
||||||
|
"Expand": "Expand",
|
||||||
"Submenu": "Submenu",
|
"Submenu": "Submenu",
|
||||||
"New": "New",
|
"New": "New",
|
||||||
"Only visible to you": "Only visible to you",
|
"Only visible to you": "Only visible to you",
|
||||||
@@ -94,7 +98,7 @@
|
|||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Invite people": "Invite people",
|
"Invite people": "Invite people",
|
||||||
"Create a collection": "Create a collection",
|
"Create a collection": "Create a collection",
|
||||||
"Return to App": "Return to App",
|
"Return to App": "Back to App",
|
||||||
"Account": "Account",
|
"Account": "Account",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Notifications": "Notifications",
|
"Notifications": "Notifications",
|
||||||
@@ -108,8 +112,6 @@
|
|||||||
"Import / Export": "Import / Export",
|
"Import / Export": "Import / Export",
|
||||||
"Integrations": "Integrations",
|
"Integrations": "Integrations",
|
||||||
"Installation": "Installation",
|
"Installation": "Installation",
|
||||||
"Expand": "Expand",
|
|
||||||
"Collapse": "Collapse",
|
|
||||||
"Unstar": "Unstar",
|
"Unstar": "Unstar",
|
||||||
"Star": "Star",
|
"Star": "Star",
|
||||||
"Appearance": "Appearance",
|
"Appearance": "Appearance",
|
||||||
@@ -204,8 +206,6 @@
|
|||||||
"The collection was updated": "The collection was updated",
|
"The collection was updated": "The collection was updated",
|
||||||
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
"You can edit the name and other details at any time, however doing so often might confuse your team mates.": "You can edit the name and other details at any time, however doing so often might confuse your team mates.",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Description": "Description",
|
|
||||||
"More details about this collection…": "More details about this collection…",
|
|
||||||
"Alphabetical": "Alphabetical",
|
"Alphabetical": "Alphabetical",
|
||||||
"Private collection": "Private collection",
|
"Private collection": "Private collection",
|
||||||
"A private collection will only be visible to invited team members.": "A private collection will only be visible to invited team members.",
|
"A private collection will only be visible to invited team members.": "A private collection will only be visible to invited team members.",
|
||||||
@@ -237,12 +237,9 @@
|
|||||||
"Never signed in": "Never signed in",
|
"Never signed in": "Never signed in",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
"Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your knowledge base. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
"Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.": "Collections are for grouping your documents. They work best when organized around a topic or internal team — Product or Engineering for example.",
|
||||||
"Creating": "Creating",
|
"Creating": "Creating",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Recently viewed": "Recently viewed",
|
|
||||||
"Created by me": "Created by me",
|
|
||||||
"Search documents": "Search documents",
|
|
||||||
"Hide contents": "Hide contents",
|
"Hide contents": "Hide contents",
|
||||||
"Show contents": "Show contents",
|
"Show contents": "Show contents",
|
||||||
"Archived": "Archived",
|
"Archived": "Archived",
|
||||||
@@ -260,6 +257,7 @@
|
|||||||
"Deleting": "Deleting",
|
"Deleting": "Deleting",
|
||||||
"I’m sure – Delete": "I’m sure – Delete",
|
"I’m sure – Delete": "I’m sure – Delete",
|
||||||
"Archiving": "Archiving",
|
"Archiving": "Archiving",
|
||||||
|
"Search documents": "Search documents",
|
||||||
"No documents found for your filters.": "No documents found for your filters.",
|
"No documents found for your filters.": "No documents found for your filters.",
|
||||||
"You’ve not got any drafts at the moment.": "You’ve not got any drafts at the moment.",
|
"You’ve not got any drafts at the moment.": "You’ve not got any drafts at the moment.",
|
||||||
"Not found": "Not found",
|
"Not found": "Not found",
|
||||||
@@ -275,6 +273,8 @@
|
|||||||
"Could not remove user": "Could not remove user",
|
"Could not remove user": "Could not remove user",
|
||||||
"Add people": "Add people",
|
"Add people": "Add people",
|
||||||
"This group has no members.": "This group has no members.",
|
"This group has no members.": "This group has no members.",
|
||||||
|
"Recently viewed": "Recently viewed",
|
||||||
|
"Created by me": "Created by me",
|
||||||
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.",
|
"Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and there’s Markdown too.",
|
||||||
"Navigation": "Navigation",
|
"Navigation": "Navigation",
|
||||||
"New document in current collection": "New document in current collection",
|
"New document in current collection": "New document in current collection",
|
||||||
|
|||||||
@@ -37,6 +37,21 @@ export default createGlobalStyle`
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: ${(props) =>
|
||||||
|
props.theme.breakpoints.tablet}px) and (display-mode: standalone) {
|
||||||
|
body:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: ${(props) => props.theme.divider};
|
||||||
|
z-index: ${(props) => props.theme.depths.pwaSeparator};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: ${(props) => props.theme.link};
|
color: ${(props) => props.theme.link};
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const base = {
|
|||||||
toasts: 5000,
|
toasts: 5000,
|
||||||
loadingIndicatorBar: 6000,
|
loadingIndicatorBar: 6000,
|
||||||
popover: 9000,
|
popover: 9000,
|
||||||
|
pwaSeparator: 10000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ const webpack = require('webpack');
|
|||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
|
const { RelativeCiAgentWebpackPlugin } = require('@relative-ci/agent');
|
||||||
const pkg = require("rich-markdown-editor/package.json");
|
const pkg = require("rich-markdown-editor/package.json");
|
||||||
|
const WebpackPwaManifest = require("webpack-pwa-manifest");
|
||||||
|
const WorkboxPlugin = require("workbox-webpack-plugin");
|
||||||
|
|
||||||
require('dotenv').config({ silent: true });
|
require('dotenv').config({ silent: true });
|
||||||
|
|
||||||
@@ -59,6 +61,30 @@ module.exports = {
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: 'server/static/index.html',
|
template: 'server/static/index.html',
|
||||||
}),
|
}),
|
||||||
|
new WebpackPwaManifest({
|
||||||
|
name: "Outline",
|
||||||
|
short_name: "Outline",
|
||||||
|
background_color: "#fff",
|
||||||
|
theme_color: "#fff",
|
||||||
|
start_url: process.env.URL,
|
||||||
|
display: "standalone",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: path.resolve("public/icon-512.png"),
|
||||||
|
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
|
||||||
|
// If only those two icon sizes are provided, Chrome will automatically scale the icons
|
||||||
|
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
|
||||||
|
// pixel-perfection, provide icons in increments of 48dp.
|
||||||
|
sizes: [512, 192],
|
||||||
|
purpose: "any maskable",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new WorkboxPlugin.GenerateSW({
|
||||||
|
clientsClaim: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, // For large bundles
|
||||||
|
}),
|
||||||
new RelativeCiAgentWebpackPlugin(),
|
new RelativeCiAgentWebpackPlugin(),
|
||||||
],
|
],
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
Reference in New Issue
Block a user