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

@@ -15,6 +15,10 @@ export const Action = styled(Flex)`
color: ${props => props.theme.text};
height: 24px;
}
&:empty {
display: none;
}
`;
export const Separator = styled.div`

View File

@@ -4,9 +4,9 @@ import styled from "styled-components";
const Badge = styled.span`
margin-left: 10px;
padding: 2px 6px 3px;
background-color: ${({ admin, theme }) =>
admin ? theme.primary : theme.textTertiary};
color: ${({ admin, theme }) => (admin ? theme.white : theme.background)};
background-color: ${({ primary, theme }) =>
primary ? theme.primary : theme.textTertiary};
color: ${({ primary, theme }) => (primary ? theme.white : theme.background)};
border-radius: 4px;
font-size: 11px;
font-weight: 500;

View File

@@ -4,7 +4,13 @@ import { observer, inject } from "mobx-react";
import breakpoint from "styled-components-breakpoint";
import styled from "styled-components";
import { Link } from "react-router-dom";
import { PadlockIcon, GoToIcon, MoreIcon } from "outline-icons";
import {
PadlockIcon,
GoToIcon,
MoreIcon,
ShapesIcon,
EditIcon,
} from "outline-icons";
import Document from "models/Document";
import CollectionsStore from "stores/CollectionsStore";
@@ -44,12 +50,32 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
);
}
const isTemplate = document.isTemplate;
const isDraft = !document.publishedAt && !isTemplate;
const isNestedDocument = path.length > 1;
const lastPath = path.length ? path[path.length - 1] : undefined;
const menuPath = isNestedDocument ? path.slice(0, -1) : [];
return (
<Wrapper justify="flex-start" align="center">
{isTemplate && (
<React.Fragment>
<CollectionName to="/templates">
<ShapesIcon color="currentColor" />&nbsp;
<span>Templates</span>
</CollectionName>
<Slash />
</React.Fragment>
)}
{isDraft && (
<React.Fragment>
<CollectionName to="/drafts">
<EditIcon color="currentColor" />&nbsp;
<span>Drafts</span>
</CollectionName>
<Slash />
</React.Fragment>
)}
<CollectionName to={collectionUrl(collection.id)}>
<CollectionIcon collection={collection} expanded />&nbsp;
<span>{collection.name}</span>

View File

@@ -113,6 +113,7 @@ export const Inner = styled.span`
line-height: ${props => (props.hasIcon ? 24 : 32)}px;
justify-content: center;
align-items: center;
min-height: 30px;
${props => props.hasIcon && props.hasText && "padding-left: 4px;"};
${props => props.hasIcon && !props.hasText && "padding: 0 4px;"};

View File

@@ -2,15 +2,17 @@
import * as React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { StarredIcon } from "outline-icons";
import { StarredIcon, PlusIcon } from "outline-icons";
import styled, { withTheme } from "styled-components";
import Flex from "components/Flex";
import Badge from "components/Badge";
import Button from "components/Button";
import Tooltip from "components/Tooltip";
import Highlight from "components/Highlight";
import PublishingInfo from "components/PublishingInfo";
import DocumentMenu from "menus/DocumentMenu";
import Document from "models/Document";
import { newDocumentUrl } from "utils/routeHelpers";
type Props = {
document: Document,
@@ -20,8 +22,117 @@ type Props = {
showPublished?: boolean,
showPin?: boolean,
showDraft?: boolean,
showTemplate?: boolean,
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
handleStar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.star();
};
handleUnstar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
render() {
const {
document,
showCollection,
showPublished,
showPin,
showDraft = true,
showTemplate,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
return (
<DocumentLink
to={{
pathname: document.url,
state: { title: document.titleWithDefault },
}}
{...rest}
>
<Heading>
<Title text={document.titleWithDefault} highlight={highlight} />
{!document.isDraft &&
!document.isArchived &&
!document.isTemplate && (
<Actions>
{document.isStarred ? (
<StyledStar onClick={this.handleUnstar} solid />
) : (
<StyledStar onClick={this.handleStar} />
)}
</Actions>
)}
{document.isDraft &&
showDraft && (
<Tooltip
tooltip="Only visible to you"
delay={500}
placement="top"
>
<Badge>Draft</Badge>
</Tooltip>
)}
{document.isTemplate &&
showTemplate && <Badge primary>Template</Badge>}
<SecondaryActions>
{document.isTemplate &&
!document.isArchived &&
!document.isDeleted && (
<Button
as={Link}
to={newDocumentUrl(document.collectionId, {
templateId: document.id,
})}
icon={<PlusIcon />}
neutral
>
New doc
</Button>
)}&nbsp;
<DocumentMenu document={document} showPin={showPin} />
</SecondaryActions>
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
document={document}
showCollection={showCollection}
showPublished={showPublished}
/>
</DocumentLink>
);
}
}
const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
<StarredIcon color={theme.text} {...props} />
))`
@@ -37,7 +148,8 @@ const StyledStar = withTheme(styled(({ solid, theme, ...props }) => (
}
`);
const StyledDocumentMenu = styled(DocumentMenu)`
const SecondaryActions = styled(Flex)`
align-items: center;
position: absolute;
right: 16px;
top: 50%;
@@ -54,7 +166,7 @@ const DocumentLink = styled(Link)`
overflow: hidden;
position: relative;
${StyledDocumentMenu} {
${SecondaryActions} {
opacity: 0;
}
@@ -64,7 +176,11 @@ const DocumentLink = styled(Link)`
background: ${props => props.theme.listItemHoverBackground};
outline: none;
${StyledStar}, ${StyledDocumentMenu} {
${SecondaryActions} {
opacity: 1;
}
${StyledStar} {
opacity: 0.5;
&:hover {
@@ -106,91 +222,4 @@ const ResultContext = styled(Highlight)`
margin-bottom: 0.25em;
`;
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@observer
class DocumentPreview extends React.Component<Props> {
star = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.star();
};
unstar = (ev: SyntheticEvent<>) => {
ev.preventDefault();
ev.stopPropagation();
this.props.document.unstar();
};
replaceResultMarks = (tag: string) => {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
};
render() {
const {
document,
showCollection,
showPublished,
showPin,
showDraft = true,
highlight,
context,
...rest
} = this.props;
const queryIsInTitle =
!!highlight &&
!!document.title.toLowerCase().includes(highlight.toLowerCase());
return (
<DocumentLink
to={{
pathname: document.url,
state: { title: document.title },
}}
{...rest}
>
<Heading>
<Title text={document.title || "Untitled"} highlight={highlight} />
{!document.isDraft &&
!document.isArchived && (
<Actions>
{document.isStarred ? (
<StyledStar onClick={this.unstar} solid />
) : (
<StyledStar onClick={this.star} />
)}
</Actions>
)}
{document.isDraft &&
showDraft && (
<Tooltip
tooltip="Only visible to you"
delay={500}
placement="top"
>
<Badge>Draft</Badge>
</Tooltip>
)}
<StyledDocumentMenu document={document} showPin={showPin} />
</Heading>
{!queryIsInTitle && (
<ResultContext
text={context}
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
processResult={this.replaceResultMarks}
/>
)}
<PublishingInfo
document={document}
showCollection={showCollection}
showPublished={showPublished}
/>
</DocumentLink>
);
}
}
export default DocumentPreview;

View File

@@ -274,4 +274,13 @@ const Menu = styled.div`
}
`;
export const Header = styled.h3`
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: ${props => props.theme.sidebarText};
letter-spacing: 0.04em;
margin: 1em 12px 0.5em;
`;
export default DropdownMenu;

View File

@@ -42,7 +42,7 @@ const MenuItem = styled.a`
margin: 0;
padding: 6px 12px;
width: 100%;
height: 32px;
min-height: 32px;
color: ${props =>
props.disabled ? props.theme.textTertiary : props.theme.textSecondary};

View File

@@ -1,3 +1,3 @@
// @flow
export { default as DropdownMenu } from "./DropdownMenu";
export { default as DropdownMenu, Header } from "./DropdownMenu";
export { default as DropdownMenuItem } from "./DropdownMenuItem";

View File

@@ -1,20 +1,8 @@
// @flow
import * as React from "react";
import styled from "styled-components";
type Props = {
children: React.Node,
};
const Empty = (props: Props) => {
const { children, ...rest } = props;
return <Container {...rest}>{children}</Container>;
};
const Container = styled.div`
display: flex;
color: ${props => props.theme.slate};
text-align: center;
const Empty = styled.p`
color: ${props => props.theme.textTertiary};
`;
export default Empty;

View File

@@ -26,7 +26,7 @@ function HoverPreviewDocument({ url, documents, children }: Props) {
return children(
<Content to={document.url}>
<Heading>{document.title}</Heading>
<Heading>{document.titleWithDefault}</Heading>
<DocumentMeta isDraft={document.isDraft} document={document} />
<Editor

View File

@@ -7,7 +7,6 @@ import CollectionNew from "scenes/CollectionNew";
import CollectionEdit from "scenes/CollectionEdit";
import CollectionDelete from "scenes/CollectionDelete";
import CollectionExport from "scenes/CollectionExport";
import DocumentDelete from "scenes/DocumentDelete";
import DocumentShare from "scenes/DocumentShare";
type Props = {
@@ -51,9 +50,6 @@ class Modals extends React.Component<Props> {
<Modal name="document-share" title="Share document">
<DocumentShare onSubmit={this.handleClose} />
</Modal>
<Modal name="document-delete" title="Delete document">
<DocumentDelete onSubmit={this.handleClose} />
</Modal>
</span>
);
}

View File

@@ -8,6 +8,7 @@ import {
EditIcon,
SearchIcon,
StarredIcon,
ShapesIcon,
TrashIcon,
PlusIcon,
} from "outline-icons";
@@ -43,6 +44,7 @@ class MainSidebar extends React.Component<Props> {
componentDidMount() {
this.props.documents.fetchDrafts();
this.props.documents.fetchTemplates();
}
handleCreateCollection = (ev: SyntheticEvent<>) => {
@@ -103,6 +105,15 @@ class MainSidebar extends React.Component<Props> {
exact={false}
label="Starred"
/>
<SidebarLink
to="/templates"
icon={<ShapesIcon color="currentColor" />}
exact={false}
label="Templates"
active={
documents.active ? documents.active.template : undefined
}
/>
<SidebarLink
to="/drafts"
icon={<EditIcon color="currentColor" />}
@@ -116,7 +127,8 @@ class MainSidebar extends React.Component<Props> {
active={
documents.active
? !documents.active.publishedAt &&
!documents.active.isDeleted
!documents.active.isDeleted &&
!documents.active.isTemplate
: undefined
}
/>

View File

@@ -10,7 +10,7 @@ type Props = {
const StyledNavLink = styled(NavLink)`
position: relative;
top: 1px;
bottom: -1px;
display: inline-block;
font-weight: 500;
@@ -33,14 +33,14 @@ const StyledNavLink = styled(NavLink)`
}
`;
function Tab(props: Props) {
function Tab({ theme, ...rest }: Props) {
const activeStyle = {
paddingBottom: "5px",
borderBottom: `3px solid ${props.theme.textSecondary}`,
color: props.theme.textSecondary,
borderBottom: `3px solid ${theme.textSecondary}`,
color: theme.textSecondary,
};
return <StyledNavLink {...props} activeStyle={activeStyle} />;
return <StyledNavLink {...rest} activeStyle={activeStyle} />;
}
export default withTheme(Tab);

View File

@@ -2,6 +2,7 @@
import styled from "styled-components";
const Tabs = styled.nav`
position: relative;
border-bottom: 1px solid ${props => props.theme.divider};
margin-top: 22px;
margin-bottom: 12px;