feat: Templates (#1399)

* Migrations
* New from template
* fix: Don't allow public share of template
* chore: Template badges
* fix: Collection active
* feat: New doc button on template list item
* feat: New template menu
* fix: Sorting
* feat: Templates onboarding notice
* fix: New doc button showing on archived/deleted templates
This commit is contained in:
Tom Moor
2020-08-08 15:18:37 -07:00
committed by GitHub
parent 59c24aba7c
commit 869fc086d6
51 changed files with 1007 additions and 327 deletions

View File

@@ -8,12 +8,13 @@ import { observer, inject } from "mobx-react";
import { Prompt, Route, withRouter } from "react-router-dom";
import type { Location, RouterHistory } from "react-router-dom";
import keydown from "react-keydown";
import { InputIcon } from "outline-icons";
import Flex from "components/Flex";
import {
collectionUrl,
documentMoveUrl,
documentHistoryUrl,
documentEditUrl,
editDocumentUrl,
documentUrl,
} from "utils/routeHelpers";
import { emojiToUrl } from "utils/emoji";
@@ -68,8 +69,6 @@ type Props = {
@observer
class DocumentScene extends React.Component<Props> {
@observable editor: ?any;
getEditorText: () => string = () => this.props.document.text;
@observable editorComponent = EditorImport;
@observable isUploading: boolean = false;
@observable isSaving: boolean = false;
@@ -79,6 +78,7 @@ class DocumentScene extends React.Component<Props> {
@observable moveModalOpen: boolean = false;
@observable lastRevision: number;
@observable title: string;
getEditorText: () => string = () => this.props.document.text;
constructor(props) {
super();
@@ -114,6 +114,12 @@ class DocumentScene extends React.Component<Props> {
}
}
if (document.injectTemplate) {
this.isDirty = true;
this.title = document.title;
document.injectTemplate = false;
}
this.updateBackground();
}
@@ -143,7 +149,7 @@ class DocumentScene extends React.Component<Props> {
const { document, abilities } = this.props;
if (abilities.update) {
this.props.history.push(documentEditUrl(document));
this.props.history.push(editDocumentUrl(document));
}
}
@@ -256,7 +262,7 @@ class DocumentScene extends React.Component<Props> {
this.props.history.push(savedDocument.url);
this.props.ui.setActiveDocument(savedDocument);
} else if (isNew) {
this.props.history.push(documentEditUrl(savedDocument));
this.props.history.push(editDocumentUrl(savedDocument));
this.props.ui.setActiveDocument(savedDocument);
}
} catch (err) {
@@ -342,6 +348,7 @@ class DocumentScene extends React.Component<Props> {
}
const value = revision ? revision.text : document.text;
const injectTemplate = document.injectTemplate;
const disableEmbeds =
(team && team.documentEmbeds === false) || document.embedsDisabled;
@@ -360,7 +367,7 @@ class DocumentScene extends React.Component<Props> {
)}
/>
<PageTitle
title={document.title.replace(document.emoji, "") || "Untitled"}
title={document.titleWithDefault.replace(document.emoji, "")}
favicon={document.emoji ? emojiToUrl(document.emoji) : undefined}
/>
{(this.isUploading || this.isSaving) && <LoadingIndicator />}
@@ -400,6 +407,15 @@ class DocumentScene extends React.Component<Props> {
column
auto
>
{document.isTemplate &&
!readOnly && (
<Notice muted>
Youre editing a template. Highlight some text and use the{" "}
<PlaceholderIcon color="currentColor" /> control to add
placeholders that can be filled out when creating new
documents from this template.
</Notice>
)}
{document.archivedAt &&
!document.deletedAt && (
<Notice muted>
@@ -414,7 +430,7 @@ class DocumentScene extends React.Component<Props> {
{document.permanentlyDeletedAt && (
<React.Fragment>
<br />
This document will be permanently deleted in{" "}
This {document.noun} will be permanently deleted in{" "}
<Time dateTime={document.permanentlyDeletedAt} /> unless
restored.
</React.Fragment>
@@ -437,7 +453,8 @@ class DocumentScene extends React.Component<Props> {
}}
isShare={isShare}
isDraft={document.isDraft}
key={disableEmbeds ? "embeds-disabled" : "embeds-enabled"}
template={document.isTemplate}
key={[injectTemplate, disableEmbeds].join("-")}
title={revision ? revision.title : this.title}
document={document}
value={readOnly ? value : undefined}
@@ -476,6 +493,11 @@ class DocumentScene extends React.Component<Props> {
}
}
const PlaceholderIcon = styled(InputIcon)`
position: relative;
top: 6px;
`;
const Background = styled(Container)`
background: ${props => props.theme.background};
transition: ${props => props.theme.backgroundTransition};

View File

@@ -80,8 +80,8 @@ class DocumentEditor extends React.Component<Props> {
type="text"
onChange={onChangeTitle}
onKeyDown={this.handleTitleKeyDown}
placeholder="Start with a title…"
value={!title && readOnly ? "Untitled" : title}
placeholder={document.placeholder}
value={!title && readOnly ? document.titleWithDefault : title}
style={startsWithEmojiAndSpace ? { marginLeft: "-1.2em" } : undefined}
readOnlyWriteCheckboxes
readOnly={readOnly}

View File

@@ -15,11 +15,12 @@ import {
import { transparentize, darken } from "polished";
import Document from "models/Document";
import AuthStore from "stores/AuthStore";
import { documentEditUrl } from "utils/routeHelpers";
import { newDocumentUrl, editDocumentUrl } from "utils/routeHelpers";
import { meta } from "utils/keyboard";
import Flex from "components/Flex";
import Breadcrumb, { Slash } from "components/Breadcrumb";
import TemplatesMenu from "menus/TemplatesMenu";
import DocumentMenu from "menus/DocumentMenu";
import NewChildDocumentMenu from "menus/NewChildDocumentMenu";
import DocumentShare from "scenes/DocumentShare";
@@ -76,7 +77,15 @@ class Header extends React.Component<Props> {
handleScroll = throttle(this.updateIsScrolled, 50);
handleEdit = () => {
this.redirectTo = documentEditUrl(this.props.document);
this.redirectTo = editDocumentUrl(this.props.document);
};
handleNewFromTemplate = () => {
const { document } = this.props;
this.redirectTo = newDocumentUrl(document.collectionId, {
templateId: document.id,
});
};
handleSave = () => {
@@ -125,6 +134,8 @@ class Header extends React.Component<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 canShareDocuments = auth.team && auth.team.sharing && can.share;
const canToggleEmbeds = auth.team && auth.team.documentEmbeds;
@@ -196,6 +207,13 @@ class Header extends React.Component<Props> {
currentUserId={auth.user ? auth.user.id : undefined}
/>
</Fade>
{isEditing &&
!isTemplate &&
isNew && (
<Action>
<TemplatesMenu document={document} />
</Action>
)}
{!isEditing &&
canShareDocuments && (
<Action>
@@ -245,31 +263,10 @@ class Header extends React.Component<Props> {
</Action>
</React.Fragment>
)}
{can.update &&
isDraft &&
!isRevision && (
<Action>
<Tooltip
tooltip="Publish"
shortcut={`${meta}+shift+p`}
delay={500}
placement="bottom"
>
<Button
onClick={this.handlePublish}
title="Publish document"
disabled={publishingIsDisabled}
small
>
{isPublishing ? "Publishing…" : "Publish"}
</Button>
</Tooltip>
</Action>
)}
{canEdit && (
<Action>
<Tooltip
tooltip="Edit document"
tooltip={`Edit ${document.noun}`}
shortcut="e"
delay={500}
placement="bottom"
@@ -305,6 +302,42 @@ class Header extends React.Component<Props> {
/>
</Action>
)}
{canEdit &&
isTemplate &&
!isDraft &&
!isRevision && (
<Action>
<Button
icon={<PlusIcon />}
onClick={this.handleNewFromTemplate}
primary
small
>
New from template
</Button>
</Action>
)}
{can.update &&
isDraft &&
!isRevision && (
<Action>
<Tooltip
tooltip="Publish"
shortcut={`${meta}+shift+p`}
delay={500}
placement="bottom"
>
<Button
onClick={this.handlePublish}
title="Publish document"
disabled={publishingIsDisabled}
small
>
{isPublishing ? "Publishing…" : "Publish"}
</Button>
</Tooltip>
</Action>
)}
{!isEditing && (
<React.Fragment>
<Separator />