WIP: Dashboard tabs
This commit is contained in:
@ -24,7 +24,13 @@ type Props = {
|
||||
};
|
||||
|
||||
function PublishingInfo({ collection, document }: Props) {
|
||||
const { modifiedSinceViewed, updatedAt, updatedBy, publishedAt } = document;
|
||||
const {
|
||||
modifiedSinceViewed,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
publishedAt,
|
||||
isDraft,
|
||||
} = document;
|
||||
|
||||
return (
|
||||
<Container align="center">
|
||||
@ -35,20 +41,20 @@ function PublishingInfo({ collection, document }: Props) {
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{updatedBy.name}
|
||||
{publishedAt ? (
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
modified <Time dateTime={updatedAt} /> ago
|
||||
</Modified>
|
||||
) : (
|
||||
{isDraft ? (
|
||||
<span>
|
||||
saved <Time dateTime={updatedAt} /> ago
|
||||
</span>
|
||||
) : (
|
||||
<Modified highlight={modifiedSinceViewed}>
|
||||
modified <Time dateTime={updatedAt} /> ago
|
||||
</Modified>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{collection && (
|
||||
<span>
|
||||
in <strong>{collection.name}</strong>
|
||||
in <strong>{isDraft ? 'Drafts' : collection.name}</strong>
|
||||
</span>
|
||||
)}
|
||||
</Container>
|
||||
|
@ -56,7 +56,7 @@ class MainSidebar extends React.Component<Props> {
|
||||
<Flex auto column>
|
||||
<Scrollable shadow>
|
||||
<Section>
|
||||
<SidebarLink to="/dashboard" icon={<HomeIcon />}>
|
||||
<SidebarLink to="/dashboard" icon={<HomeIcon />} exact={false}>
|
||||
Home
|
||||
</SidebarLink>
|
||||
<SidebarLink to="/search" icon={<SearchIcon />}>
|
||||
|
@ -52,6 +52,7 @@ type Props = {
|
||||
iconColor?: string,
|
||||
active?: boolean,
|
||||
theme: Object,
|
||||
exact?: boolean,
|
||||
};
|
||||
|
||||
@observer
|
||||
@ -100,6 +101,7 @@ class SidebarLink extends React.Component<Props> {
|
||||
menu,
|
||||
menuOpen,
|
||||
hideExpandToggle,
|
||||
exact,
|
||||
} = this.props;
|
||||
const Component = to ? StyledNavLink : StyledDiv;
|
||||
const showExpandIcon =
|
||||
@ -113,7 +115,7 @@ class SidebarLink extends React.Component<Props> {
|
||||
style={active ? this.activeStyle : undefined}
|
||||
onClick={onClick}
|
||||
to={to}
|
||||
exact
|
||||
exact={exact !== false}
|
||||
>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
{showExpandIcon && (
|
||||
|
26
app/components/Tab.js
Normal file
26
app/components/Tab.js
Normal file
@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
const NavItem = styled(NavLink)`
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: ${props => props.theme.slate};
|
||||
letter-spacing: 0.04em;
|
||||
margin-right: 20px;
|
||||
padding-bottom: 8px;
|
||||
`;
|
||||
|
||||
function Tab(props: *) {
|
||||
const activeStyle = {
|
||||
paddingBottom: '5px',
|
||||
borderBottom: `3px solid ${props.theme.slateLight}`,
|
||||
};
|
||||
|
||||
return <NavItem {...props} activeStyle={activeStyle} />;
|
||||
}
|
||||
|
||||
export default withTheme(Tab);
|
10
app/components/Tabs.js
Normal file
10
app/components/Tabs.js
Normal file
@ -0,0 +1,10 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Tabs = styled.nav`
|
||||
border-bottom: 1px solid ${props => props.theme.slateLight};
|
||||
margin-top: 22px;
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
export default Tabs;
|
122
app/index.js
122
app/index.js
@ -3,54 +3,23 @@ import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'mobx-react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Redirect,
|
||||
} from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import stores from 'stores';
|
||||
import theme from 'shared/styles/theme';
|
||||
import globalStyles from 'shared/styles/globals';
|
||||
import 'shared/styles/prism.css';
|
||||
|
||||
import Home from 'scenes/Home';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
import Starred from 'scenes/Starred';
|
||||
import Drafts from 'scenes/Drafts';
|
||||
import Collection from 'scenes/Collection';
|
||||
import Document from 'scenes/Document';
|
||||
import Search from 'scenes/Search';
|
||||
import Settings from 'scenes/Settings';
|
||||
import Details from 'scenes/Settings/Details';
|
||||
import People from 'scenes/Settings/People';
|
||||
import Slack from 'scenes/Settings/Slack';
|
||||
import Shares from 'scenes/Settings/Shares';
|
||||
import Tokens from 'scenes/Settings/Tokens';
|
||||
import Export from 'scenes/Settings/Export';
|
||||
import Error404 from 'scenes/Error404';
|
||||
|
||||
import ErrorBoundary from 'components/ErrorBoundary';
|
||||
import ScrollToTop from 'components/ScrollToTop';
|
||||
import ScrollToAnchor from 'components/ScrollToAnchor';
|
||||
import Layout from 'components/Layout';
|
||||
import Auth from 'components/Auth';
|
||||
import RouteSidebarHidden from 'components/RouteSidebarHidden';
|
||||
|
||||
import { matchDocumentSlug } from 'utils/routeHelpers';
|
||||
import Routes from './routes';
|
||||
|
||||
let DevTools;
|
||||
if (__DEV__) {
|
||||
DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
const notFoundSearch = () => <Search notFound />;
|
||||
const DocumentNew = () => <Document newDocument />;
|
||||
const RedirectDocument = ({ match }: { match: Object }) => (
|
||||
<Redirect to={`/doc/${match.params.documentSlug}`} />
|
||||
);
|
||||
|
||||
globalStyles();
|
||||
|
||||
const element = document.getElementById('root');
|
||||
@ -64,92 +33,7 @@ if (element) {
|
||||
<Router>
|
||||
<ScrollToTop>
|
||||
<ScrollToAnchor>
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/share/:shareId" component={Document} />
|
||||
<Auth>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard"
|
||||
component={Dashboard}
|
||||
/>
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/details"
|
||||
component={Details}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/people"
|
||||
component={People}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/shares"
|
||||
component={Shares}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/tokens"
|
||||
component={Tokens}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/export"
|
||||
component={Export}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/collections/:id"
|
||||
component={Collection}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/d/${matchDocumentSlug}`}
|
||||
component={RedirectDocument}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/move`}
|
||||
component={Document}
|
||||
/>
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route
|
||||
exact
|
||||
path="/search/:query"
|
||||
component={Search}
|
||||
/>
|
||||
<Route path="/404" component={Error404} />
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path={`/doc/${matchDocumentSlug}/edit`}
|
||||
component={Document}
|
||||
/>
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path="/collections/:id/new"
|
||||
component={DocumentNew}
|
||||
/>
|
||||
<Route component={notFoundSearch} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</Auth>
|
||||
</Switch>
|
||||
<Routes />
|
||||
</ScrollToAnchor>
|
||||
</ScrollToTop>
|
||||
</Router>
|
||||
|
78
app/routes.js
Normal file
78
app/routes.js
Normal file
@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
|
||||
import Home from 'scenes/Home';
|
||||
import Dashboard from 'scenes/Dashboard';
|
||||
import Starred from 'scenes/Starred';
|
||||
import Drafts from 'scenes/Drafts';
|
||||
import Collection from 'scenes/Collection';
|
||||
import Document from 'scenes/Document';
|
||||
import Search from 'scenes/Search';
|
||||
import Settings from 'scenes/Settings';
|
||||
import Details from 'scenes/Settings/Details';
|
||||
import People from 'scenes/Settings/People';
|
||||
import Slack from 'scenes/Settings/Slack';
|
||||
import Shares from 'scenes/Settings/Shares';
|
||||
import Tokens from 'scenes/Settings/Tokens';
|
||||
import Export from 'scenes/Settings/Export';
|
||||
import Error404 from 'scenes/Error404';
|
||||
|
||||
import Layout from 'components/Layout';
|
||||
import Auth from 'components/Auth';
|
||||
import RouteSidebarHidden from 'components/RouteSidebarHidden';
|
||||
import { matchDocumentSlug as slug } from 'utils/routeHelpers';
|
||||
|
||||
const NotFound = () => <Search notFound />;
|
||||
const DocumentNew = () => <Document newDocument />;
|
||||
const RedirectDocument = ({ match }: { match: Object }) => (
|
||||
<Redirect to={`/doc/${match.params.documentSlug}`} />
|
||||
);
|
||||
|
||||
export default function Routes() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/" component={Home} />
|
||||
<Route exact path="/share/:shareId" component={Document} />
|
||||
<Auth>
|
||||
<Layout>
|
||||
<Switch>
|
||||
<Route path="/dashboard/:tab" component={Dashboard} />
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route exact path="/starred" component={Starred} />
|
||||
<Route exact path="/drafts" component={Drafts} />
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/details" component={Details} />
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/shares" component={Shares} />
|
||||
<Route exact path="/settings/tokens" component={Tokens} />
|
||||
<Route
|
||||
exact
|
||||
path="/settings/integrations/slack"
|
||||
component={Slack}
|
||||
/>
|
||||
<Route exact path="/settings/export" component={Export} />
|
||||
<Route exact path="/collections/:id" component={Collection} />
|
||||
<Route exact path={`/d/${slug}`} component={RedirectDocument} />
|
||||
<Route exact path={`/doc/${slug}`} component={Document} />
|
||||
<Route exact path={`/doc/${slug}/move`} component={Document} />
|
||||
<Route exact path="/search" component={Search} />
|
||||
<Route exact path="/search/:query" component={Search} />
|
||||
<Route path="/404" component={Error404} />
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path={`/doc/${slug}/edit`}
|
||||
component={Document}
|
||||
/>
|
||||
<RouteSidebarHidden
|
||||
exact
|
||||
path="/collections/:id/new"
|
||||
component={DocumentNew}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</Auth>
|
||||
</Switch>
|
||||
);
|
||||
}
|
@ -1,20 +1,25 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { NewDocumentIcon } from 'outline-icons';
|
||||
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import NewDocumentMenu from 'menus/NewDocumentMenu';
|
||||
import Actions, { Action } from 'components/Actions';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import DocumentList from 'components/DocumentList';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Subheading from 'components/Subheading';
|
||||
import Tabs from 'components/Tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
|
||||
type Props = {
|
||||
documents: DocumentsStore,
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
@ -27,16 +32,20 @@ class Dashboard extends React.Component<Props> {
|
||||
|
||||
loadContent = async () => {
|
||||
await Promise.all([
|
||||
this.props.documents.fetchRecentlyModified({ limit: 5 }),
|
||||
this.props.documents.fetchRecentlyViewed({ limit: 5 }),
|
||||
this.props.documents.fetchRecentlyModified({ limit: 15 }),
|
||||
this.props.documents.fetchRecentlyViewed({ limit: 15 }),
|
||||
]);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
const { documents, auth } = this.props;
|
||||
if (!auth.user) return;
|
||||
|
||||
const hasRecentlyViewed = documents.recentlyViewed.length > 0;
|
||||
const hasRecentlyEdited = documents.recentlyEdited.length > 0;
|
||||
|
||||
const owned = documents.owned(auth.user.id);
|
||||
const showContent =
|
||||
this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited);
|
||||
|
||||
@ -44,28 +53,34 @@ class Dashboard extends React.Component<Props> {
|
||||
<CenteredContent>
|
||||
<PageTitle title="Home" />
|
||||
<h1>Home</h1>
|
||||
<Tabs>
|
||||
<Tab to="/dashboard" exact>
|
||||
Recently edited
|
||||
</Tab>
|
||||
<Tab to="/dashboard/recent" exact>
|
||||
Recently viewed
|
||||
</Tab>
|
||||
<Tab to="/dashboard/owned">Created by me</Tab>
|
||||
</Tabs>
|
||||
{showContent ? (
|
||||
<React.Fragment>
|
||||
{hasRecentlyViewed && (
|
||||
<React.Fragment>
|
||||
<Subheading key="viewed">Recently viewed</Subheading>
|
||||
<Switch>
|
||||
<Route path="/dashboard/recent">
|
||||
<DocumentList
|
||||
key="viewedDocuments"
|
||||
documents={documents.recentlyViewed}
|
||||
showCollection
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{hasRecentlyEdited && (
|
||||
<React.Fragment>
|
||||
<Subheading key="edited">Recently edited</Subheading>
|
||||
</Route>
|
||||
<Route path="/dashboard/owned">
|
||||
<DocumentList documents={owned} showCollection />
|
||||
</Route>
|
||||
<Route path="/dashboard">
|
||||
<DocumentList
|
||||
key="editedDocuments"
|
||||
documents={documents.recentlyEdited}
|
||||
showCollection
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Route>
|
||||
</Switch>
|
||||
<Actions align="center" justify="flex-end">
|
||||
<Action>
|
||||
<NewDocumentMenu label={<NewDocumentIcon />} />
|
||||
@ -80,4 +95,4 @@ class Dashboard extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('documents')(Dashboard);
|
||||
export default inject('documents', 'auth')(Dashboard);
|
||||
|
@ -29,10 +29,8 @@ class DocumentsStore extends BaseStore {
|
||||
|
||||
ui: UiStore;
|
||||
|
||||
/* Computed */
|
||||
|
||||
@computed
|
||||
get recentlyViewed(): Array<Document> {
|
||||
get recentlyViewed(): Document[] {
|
||||
const docs = [];
|
||||
this.recentlyViewedIds.forEach(id => {
|
||||
const doc = this.getById(id);
|
||||
@ -51,6 +49,17 @@ class DocumentsStore extends BaseStore {
|
||||
return docs;
|
||||
}
|
||||
|
||||
owned(userId: string): Document[] {
|
||||
return _.orderBy(
|
||||
_.filter(
|
||||
this.data.values(),
|
||||
document => document.createdBy.id === userId
|
||||
),
|
||||
'updatedAt',
|
||||
'desc'
|
||||
);
|
||||
}
|
||||
|
||||
pinnedInCollection(collectionId: string): Document[] {
|
||||
return _.filter(
|
||||
this.recentlyEditedInCollection(collectionId),
|
||||
@ -152,6 +161,11 @@ class DocumentsStore extends BaseStore {
|
||||
await this.fetchPage('pinned', options);
|
||||
};
|
||||
|
||||
@action
|
||||
fetchOwned = async (options: ?PaginationParams): Promise<*> => {
|
||||
await this.fetchPage('owned', options);
|
||||
};
|
||||
|
||||
@action
|
||||
search = async (
|
||||
query: string,
|
||||
|
Reference in New Issue
Block a user