chore: Standardized headers (#1883)

* feat: Collection to standard header
feat: Sticky tabs

* chore: Document to standard header

* chore: Dashboard -> Home
chore: Scene component

* chore: Trash, Templates, Drafts

* fix: Mobile improvements

* fix: Content showing at sides and occassionally ontop of sticky headers
This commit is contained in:
Tom Moor 2021-02-14 13:18:33 -08:00 committed by GitHub
parent 32a298054d
commit 4b603460cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 711 additions and 610 deletions

View File

@ -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 20px" : "60px 20px")};
${breakpoint("tablet")` ${breakpoint("tablet")`
padding: 60px; padding: ${(props) => (props.withStickyHeader ? "4px 60px" : "60px")};
`}; `};
`; `;

View File

@ -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);

104
app/components/Header.js Normal file
View 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);

View File

@ -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`

50
app/components/Scene.js Normal file
View 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}&nbsp;{title}
</>
) : (
title
)
}
actions={actions}
breadcrumb={breadcrumb}
/>
<CenteredContent withStickyHeader>{children}</CenteredContent>
</FillWidth>
);
}
const FillWidth = styled.div`
width: 100%;
`;
export default Scene;

View File

@ -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}

View File

@ -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")}

View File

@ -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;

View File

@ -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-top: 7px;
padding-bottom: 5px; padding-bottom: 5px;
`; `;
// 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: ${(props) => props.theme.depths.stickyHeader};
`;
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>
); );
}; };

View File

@ -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);

View File

@ -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: ${(props) => props.theme.depths.stickyHeader};
`;
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;

View File

@ -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} />

View File

@ -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

View File

@ -12,11 +12,10 @@ 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 CollectionDescription from "components/CollectionDescription";
@ -29,8 +28,8 @@ 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";
@ -126,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>
@ -174,7 +173,7 @@ class CollectionScene extends React.Component<Props> {
)} )}
/> />
</Action> </Action>
</Actions> </>
); );
} }
@ -190,169 +189,170 @@ class CollectionScene extends React.Component<Props> {
const collectionName = collection ? collection.name : ""; const collectionName = collection ? collection.name : "";
const hasPinnedDocuments = !!pinnedDocuments.length; const hasPinnedDocuments = !!pinnedDocuments.length;
return ( return collection ? (
<CenteredContent> <Scene
{collection ? ( textTitle={collection.name}
title={
<> <>
<PageTitle title={collection.name} /> <CollectionIcon collection={collection} expanded />
{collection.isEmpty ? ( &nbsp;
<Centered column> {collection.name}
<HelpText>
<Trans
defaults="<em>{{ collectionName }}</em> doesnt 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="currentColor" />}>
{t("Create a document")}
</Button>
</Link>
&nbsp;&nbsp;
{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>
<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("AZ")}
</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> doesnt 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>
&nbsp;&nbsp;
{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("AZ")}
</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>
); );
} }
@ -371,7 +371,7 @@ 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;
`; `;

View File

@ -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>
)}
&nbsp;
<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>
)}
&nbsp;
<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)
);

View File

@ -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>
); );
} }
} }

View File

@ -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);

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

@ -240,9 +240,6 @@
"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.", "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",
"Im sure  Delete": "Im sure  Delete", "Im sure  Delete": "Im 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.",
"Youve not got any drafts at the moment.": "Youve not got any drafts at the moment.", "Youve not got any drafts at the moment.": "Youve 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 theres Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too.", "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres Markdown too.": "Outline is designed to be fast and easy to use. All of your usual keyboard shortcuts work here, and theres 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",

View File

@ -108,6 +108,7 @@ export const base = {
depths: { depths: {
sidebar: 1000, sidebar: 1000,
stickyHeader: 1500,
modalOverlay: 2000, modalOverlay: 2000,
modal: 3000, modal: 3000,
menu: 4000, menu: 4000,