34
app/components/DocumentList.js
Normal file
34
app/components/DocumentList.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import Document from 'models/Document';
|
||||||
|
import DocumentPreview from 'components/DocumentPreview';
|
||||||
|
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
documents: Document[],
|
||||||
|
showCollection?: boolean,
|
||||||
|
limit?: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DocumentList({
|
||||||
|
limit,
|
||||||
|
showCollection,
|
||||||
|
documents,
|
||||||
|
}: Props) {
|
||||||
|
const items = limit ? documents.splice(0, limit) : documents;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArrowKeyNavigation
|
||||||
|
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||||
|
defaultActiveChildIndex={0}
|
||||||
|
>
|
||||||
|
{items.map(document => (
|
||||||
|
<DocumentPreview
|
||||||
|
key={document.id}
|
||||||
|
document={document}
|
||||||
|
showCollection={showCollection}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ArrowKeyNavigation>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import * as React from 'react';
|
|
||||||
import Document from 'models/Document';
|
|
||||||
import DocumentPreview from 'components/DocumentPreview';
|
|
||||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
documents: Document[],
|
|
||||||
showCollection?: boolean,
|
|
||||||
limit?: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
class DocumentList extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
const { limit, showCollection } = this.props;
|
|
||||||
const documents = limit
|
|
||||||
? this.props.documents.splice(0, limit)
|
|
||||||
: this.props.documents;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ArrowKeyNavigation
|
|
||||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
|
||||||
defaultActiveChildIndex={0}
|
|
||||||
>
|
|
||||||
{documents.map(document => (
|
|
||||||
<DocumentPreview
|
|
||||||
key={document.id}
|
|
||||||
document={document}
|
|
||||||
showCollection={showCollection}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ArrowKeyNavigation>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocumentList;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import DocumentList from './DocumentList';
|
|
||||||
export default DocumentList;
|
|
||||||
@@ -24,31 +24,38 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function PublishingInfo({ collection, document }: Props) {
|
function PublishingInfo({ collection, document }: Props) {
|
||||||
const { modifiedSinceViewed, updatedAt, updatedBy, publishedAt } = document;
|
const {
|
||||||
|
modifiedSinceViewed,
|
||||||
|
updatedAt,
|
||||||
|
updatedBy,
|
||||||
|
publishedAt,
|
||||||
|
isDraft,
|
||||||
|
} = document;
|
||||||
|
const neverUpdated = publishedAt === updatedAt;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container align="center">
|
<Container align="center">
|
||||||
{publishedAt && publishedAt === updatedAt ? (
|
{publishedAt && neverUpdated ? (
|
||||||
<span>
|
<span>
|
||||||
{updatedBy.name} published <Time dateTime={publishedAt} /> ago
|
{updatedBy.name} published <Time dateTime={publishedAt} /> ago
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{updatedBy.name}
|
{updatedBy.name}
|
||||||
{publishedAt ? (
|
{isDraft ? (
|
||||||
<Modified highlight={modifiedSinceViewed}>
|
|
||||||
modified <Time dateTime={updatedAt} /> ago
|
|
||||||
</Modified>
|
|
||||||
) : (
|
|
||||||
<span>
|
<span>
|
||||||
saved <Time dateTime={updatedAt} /> ago
|
saved <Time dateTime={updatedAt} /> ago
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
<Modified highlight={modifiedSinceViewed}>
|
||||||
|
modified <Time dateTime={updatedAt} /> ago
|
||||||
|
</Modified>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{collection && (
|
{collection && (
|
||||||
<span>
|
<span>
|
||||||
in <strong>{collection.name}</strong>
|
in <strong>{isDraft ? 'Drafts' : collection.name}</strong>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
78
app/components/PaginatedDocumentList.js
Normal file
78
app/components/PaginatedDocumentList.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observable, action } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import Waypoint from 'react-waypoint';
|
||||||
|
|
||||||
|
import { DEFAULT_PAGINATION_LIMIT } from 'stores/DocumentsStore';
|
||||||
|
import Document from 'models/Document';
|
||||||
|
import DocumentList from 'components/DocumentList';
|
||||||
|
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
showCollection?: boolean,
|
||||||
|
documents: Document[],
|
||||||
|
fetch: (options: ?Object) => Promise<*>,
|
||||||
|
options?: Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class PaginatedDocumentList extends React.Component<Props> {
|
||||||
|
@observable isLoaded: boolean = false;
|
||||||
|
@observable isFetching: boolean = false;
|
||||||
|
@observable offset: number = 0;
|
||||||
|
@observable allowLoadMore: boolean = true;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.fetch !== this.props.fetch) {
|
||||||
|
this.fetchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchResults = async () => {
|
||||||
|
this.isFetching = true;
|
||||||
|
|
||||||
|
const limit = DEFAULT_PAGINATION_LIMIT;
|
||||||
|
const results = await this.props.fetch({ limit, ...this.props.options });
|
||||||
|
|
||||||
|
if (
|
||||||
|
results &&
|
||||||
|
(results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT)
|
||||||
|
) {
|
||||||
|
this.allowLoadMore = false;
|
||||||
|
} else {
|
||||||
|
this.offset += DEFAULT_PAGINATION_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoaded = true;
|
||||||
|
this.isFetching = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
loadMoreResults = async () => {
|
||||||
|
// Don't paginate if there aren't more results or we’re in the middle of fetching
|
||||||
|
if (!this.allowLoadMore || this.isFetching) return;
|
||||||
|
await this.fetchResults();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { showCollection, documents } = this.props;
|
||||||
|
|
||||||
|
return this.isLoaded || documents.length ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<DocumentList documents={documents} showCollection={showCollection} />
|
||||||
|
{this.allowLoadMore && (
|
||||||
|
<Waypoint key={this.offset} onEnter={this.loadMoreResults} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<ListPlaceholder count={5} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaginatedDocumentList;
|
||||||
@@ -56,7 +56,7 @@ class MainSidebar extends React.Component<Props> {
|
|||||||
<Flex auto column>
|
<Flex auto column>
|
||||||
<Scrollable shadow>
|
<Scrollable shadow>
|
||||||
<Section>
|
<Section>
|
||||||
<SidebarLink to="/dashboard" icon={<HomeIcon />}>
|
<SidebarLink to="/dashboard" icon={<HomeIcon />} exact={false}>
|
||||||
Home
|
Home
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/search" icon={<SearchIcon />}>
|
<SidebarLink to="/search" icon={<SearchIcon />}>
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ type Props = {
|
|||||||
iconColor?: string,
|
iconColor?: string,
|
||||||
active?: boolean,
|
active?: boolean,
|
||||||
theme: Object,
|
theme: Object,
|
||||||
|
exact?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@@ -100,6 +101,7 @@ class SidebarLink extends React.Component<Props> {
|
|||||||
menu,
|
menu,
|
||||||
menuOpen,
|
menuOpen,
|
||||||
hideExpandToggle,
|
hideExpandToggle,
|
||||||
|
exact,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const Component = to ? StyledNavLink : StyledDiv;
|
const Component = to ? StyledNavLink : StyledDiv;
|
||||||
const showExpandIcon =
|
const showExpandIcon =
|
||||||
@@ -113,7 +115,7 @@ class SidebarLink extends React.Component<Props> {
|
|||||||
style={active ? this.activeStyle : undefined}
|
style={active ? this.activeStyle : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
to={to}
|
to={to}
|
||||||
exact
|
exact={exact !== false}
|
||||||
>
|
>
|
||||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||||
{showExpandIcon && (
|
{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 { render } from 'react-dom';
|
||||||
import { Provider } from 'mobx-react';
|
import { Provider } from 'mobx-react';
|
||||||
import { ThemeProvider } from 'styled-components';
|
import { ThemeProvider } from 'styled-components';
|
||||||
import {
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
BrowserRouter as Router,
|
|
||||||
Switch,
|
|
||||||
Route,
|
|
||||||
Redirect,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import stores from 'stores';
|
import stores from 'stores';
|
||||||
import theme from 'shared/styles/theme';
|
import theme from 'shared/styles/theme';
|
||||||
import globalStyles from 'shared/styles/globals';
|
import globalStyles from 'shared/styles/globals';
|
||||||
import 'shared/styles/prism.css';
|
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 ErrorBoundary from 'components/ErrorBoundary';
|
||||||
import ScrollToTop from 'components/ScrollToTop';
|
import ScrollToTop from 'components/ScrollToTop';
|
||||||
import ScrollToAnchor from 'components/ScrollToAnchor';
|
import ScrollToAnchor from 'components/ScrollToAnchor';
|
||||||
import Layout from 'components/Layout';
|
import Routes from './routes';
|
||||||
import Auth from 'components/Auth';
|
|
||||||
import RouteSidebarHidden from 'components/RouteSidebarHidden';
|
|
||||||
|
|
||||||
import { matchDocumentSlug } from 'utils/routeHelpers';
|
|
||||||
|
|
||||||
let DevTools;
|
let DevTools;
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
DevTools = require('mobx-react-devtools').default; // eslint-disable-line global-require
|
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();
|
globalStyles();
|
||||||
|
|
||||||
const element = document.getElementById('root');
|
const element = document.getElementById('root');
|
||||||
@@ -64,92 +33,7 @@ if (element) {
|
|||||||
<Router>
|
<Router>
|
||||||
<ScrollToTop>
|
<ScrollToTop>
|
||||||
<ScrollToAnchor>
|
<ScrollToAnchor>
|
||||||
<Switch>
|
<Routes />
|
||||||
<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>
|
|
||||||
</ScrollToAnchor>
|
</ScrollToAnchor>
|
||||||
</ScrollToTop>
|
</ScrollToTop>
|
||||||
</Router>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ class CollectionScene extends React.Component<Props> {
|
|||||||
this.collection = collection;
|
this.collection = collection;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.props.documents.fetchRecentlyModified({
|
this.props.documents.fetchRecentlyEdited({
|
||||||
limit: 10,
|
limit: 10,
|
||||||
collection: id,
|
collection: id,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,83 +1,75 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { observable } from 'mobx';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
import { observer, inject } from 'mobx-react';
|
import { observer, inject } from 'mobx-react';
|
||||||
import { NewDocumentIcon } from 'outline-icons';
|
import { NewDocumentIcon } from 'outline-icons';
|
||||||
|
|
||||||
import DocumentsStore from 'stores/DocumentsStore';
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
import NewDocumentMenu from 'menus/NewDocumentMenu';
|
import NewDocumentMenu from 'menus/NewDocumentMenu';
|
||||||
import Actions, { Action } from 'components/Actions';
|
import Actions, { Action } from 'components/Actions';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import DocumentList from 'components/DocumentList';
|
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import Subheading from 'components/Subheading';
|
import Tabs from 'components/Tabs';
|
||||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
import Tab from 'components/Tab';
|
||||||
|
import PaginatedDocumentList from '../components/PaginatedDocumentList';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
documents: DocumentsStore,
|
documents: DocumentsStore,
|
||||||
|
auth: AuthStore,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Dashboard extends React.Component<Props> {
|
class Dashboard extends React.Component<Props> {
|
||||||
@observable isLoaded: boolean = false;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.loadContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadContent = async () => {
|
|
||||||
await Promise.all([
|
|
||||||
this.props.documents.fetchRecentlyModified({ limit: 5 }),
|
|
||||||
this.props.documents.fetchRecentlyViewed({ limit: 5 }),
|
|
||||||
]);
|
|
||||||
this.isLoaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { documents } = this.props;
|
const { documents, auth } = this.props;
|
||||||
const hasRecentlyViewed = documents.recentlyViewed.length > 0;
|
if (!auth.user) return;
|
||||||
const hasRecentlyEdited = documents.recentlyEdited.length > 0;
|
const user = auth.user.id;
|
||||||
const showContent =
|
|
||||||
this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredContent>
|
<CenteredContent>
|
||||||
<PageTitle title="Home" />
|
<PageTitle title="Home" />
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
{showContent ? (
|
<Tabs>
|
||||||
<React.Fragment>
|
<Tab to="/dashboard" exact>
|
||||||
{hasRecentlyViewed && (
|
Recently updated
|
||||||
<React.Fragment>
|
</Tab>
|
||||||
<Subheading key="viewed">Recently viewed</Subheading>
|
<Tab to="/dashboard/recent" exact>
|
||||||
<DocumentList
|
Recently viewed
|
||||||
key="viewedDocuments"
|
</Tab>
|
||||||
documents={documents.recentlyViewed}
|
<Tab to="/dashboard/created">Created by me</Tab>
|
||||||
showCollection
|
</Tabs>
|
||||||
/>
|
<Switch>
|
||||||
</React.Fragment>
|
<Route path="/dashboard/recent">
|
||||||
)}
|
<PaginatedDocumentList
|
||||||
{hasRecentlyEdited && (
|
key="recent"
|
||||||
<React.Fragment>
|
documents={documents.recentlyViewed}
|
||||||
<Subheading key="edited">Recently edited</Subheading>
|
fetch={documents.fetchRecentlyViewed}
|
||||||
<DocumentList
|
/>
|
||||||
key="editedDocuments"
|
</Route>
|
||||||
documents={documents.recentlyEdited}
|
<Route path="/dashboard/created">
|
||||||
showCollection
|
<PaginatedDocumentList
|
||||||
/>
|
key="created"
|
||||||
</React.Fragment>
|
documents={documents.createdByUser(user)}
|
||||||
)}
|
fetch={documents.fetchOwned}
|
||||||
<Actions align="center" justify="flex-end">
|
options={{ user }}
|
||||||
<Action>
|
/>
|
||||||
<NewDocumentMenu label={<NewDocumentIcon />} />
|
</Route>
|
||||||
</Action>
|
<Route path="/dashboard">
|
||||||
</Actions>
|
<PaginatedDocumentList
|
||||||
</React.Fragment>
|
documents={documents.recentlyEdited}
|
||||||
) : (
|
fetch={documents.fetchRecentlyEdited}
|
||||||
<ListPlaceholder count={5} />
|
/>
|
||||||
)}
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
<Actions align="center" justify="flex-end">
|
||||||
|
<Action>
|
||||||
|
<NewDocumentMenu label={<NewDocumentIcon />} />
|
||||||
|
</Action>
|
||||||
|
</Actions>
|
||||||
</CenteredContent>
|
</CenteredContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default inject('documents')(Dashboard);
|
export default inject('documents', 'auth')(Dashboard);
|
||||||
|
|||||||
@@ -21,18 +21,16 @@ type FetchOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class DocumentsStore extends BaseStore {
|
class DocumentsStore extends BaseStore {
|
||||||
@observable recentlyViewedIds: Array<string> = [];
|
@observable recentlyViewedIds: string[] = [];
|
||||||
@observable recentlyEditedIds: Array<string> = [];
|
@observable recentlyEditedIds: string[] = [];
|
||||||
@observable data: Map<string, Document> = new ObservableMap([]);
|
@observable data: Map<string, Document> = new ObservableMap([]);
|
||||||
@observable isLoaded: boolean = false;
|
@observable isLoaded: boolean = false;
|
||||||
@observable isFetching: boolean = false;
|
@observable isFetching: boolean = false;
|
||||||
|
|
||||||
ui: UiStore;
|
ui: UiStore;
|
||||||
|
|
||||||
/* Computed */
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get recentlyViewed(): Array<Document> {
|
get recentlyViewed(): Document[] {
|
||||||
const docs = [];
|
const docs = [];
|
||||||
this.recentlyViewedIds.forEach(id => {
|
this.recentlyViewedIds.forEach(id => {
|
||||||
const doc = this.getById(id);
|
const doc = this.getById(id);
|
||||||
@@ -51,6 +49,17 @@ class DocumentsStore extends BaseStore {
|
|||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdByUser(userId: string): Document[] {
|
||||||
|
return _.orderBy(
|
||||||
|
_.filter(
|
||||||
|
this.data.values(),
|
||||||
|
document => document.createdBy.id === userId
|
||||||
|
),
|
||||||
|
'updatedAt',
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pinnedInCollection(collectionId: string): Document[] {
|
pinnedInCollection(collectionId: string): Document[] {
|
||||||
return _.filter(
|
return _.filter(
|
||||||
this.recentlyEditedInCollection(collectionId),
|
this.recentlyEditedInCollection(collectionId),
|
||||||
@@ -118,10 +127,10 @@ class DocumentsStore extends BaseStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fetchRecentlyModified = async (options: ?PaginationParams): Promise<*> => {
|
fetchRecentlyEdited = async (options: ?PaginationParams): Promise<*> => {
|
||||||
const data = await this.fetchPage('list', options);
|
const data = await this.fetchPage('list', options);
|
||||||
|
|
||||||
runInAction('DocumentsStore#fetchRecentlyModified', () => {
|
runInAction('DocumentsStore#fetchRecentlyEdited', () => {
|
||||||
this.recentlyEditedIds = _.map(data, 'id');
|
this.recentlyEditedIds = _.map(data, 'id');
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -138,18 +147,23 @@ class DocumentsStore extends BaseStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fetchStarred = async (options: ?PaginationParams): Promise<*> => {
|
fetchStarred = (options: ?PaginationParams): Promise<*> => {
|
||||||
await this.fetchPage('starred', options);
|
return this.fetchPage('starred', options);
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fetchDrafts = async (options: ?PaginationParams): Promise<*> => {
|
fetchDrafts = (options: ?PaginationParams): Promise<*> => {
|
||||||
await this.fetchPage('drafts', options);
|
return this.fetchPage('drafts', options);
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fetchPinned = async (options: ?PaginationParams): Promise<*> => {
|
fetchPinned = (options: ?PaginationParams): Promise<*> => {
|
||||||
await this.fetchPage('pinned', options);
|
return this.fetchPage('pinned', options);
|
||||||
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
fetchOwned = (options: ?PaginationParams): Promise<*> => {
|
||||||
|
return this.fetchPage('list', options);
|
||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -261,11 +275,11 @@ class DocumentsStore extends BaseStore {
|
|||||||
|
|
||||||
// Re-fetch dashboard content so that we don't show deleted documents
|
// Re-fetch dashboard content so that we don't show deleted documents
|
||||||
this.on('collections.delete', () => {
|
this.on('collections.delete', () => {
|
||||||
this.fetchRecentlyModified();
|
this.fetchRecentlyEdited();
|
||||||
this.fetchRecentlyViewed();
|
this.fetchRecentlyViewed();
|
||||||
});
|
});
|
||||||
this.on('documents.delete', () => {
|
this.on('documents.delete', () => {
|
||||||
this.fetchRecentlyModified();
|
this.fetchRecentlyEdited();
|
||||||
this.fetchRecentlyViewed();
|
this.fetchRecentlyViewed();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ const { authorize, cannot } = policy;
|
|||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||||
let { sort = 'updatedAt', direction, collection } = ctx.body;
|
let { sort = 'updatedAt', direction, collection, user } = ctx.body;
|
||||||
if (direction !== 'ASC') direction = 'DESC';
|
if (direction !== 'ASC') direction = 'DESC';
|
||||||
|
|
||||||
const user = ctx.state.user;
|
let where = { teamId: ctx.state.user.teamId };
|
||||||
let where = { teamId: user.teamId };
|
|
||||||
if (collection) where = { ...where, collectionId: collection };
|
if (collection) where = { ...where, collectionId: collection };
|
||||||
|
if (user) where = { ...where, createdById: user };
|
||||||
|
|
||||||
const starredScope = { method: ['withStarred', user.id] };
|
const starredScope = { method: ['withStarred', ctx.state.user.id] };
|
||||||
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
const documents = await Document.scope('defaultScope', starredScope).findAll({
|
||||||
where,
|
where,
|
||||||
order: [[sort, direction]],
|
order: [[sort, direction]],
|
||||||
|
|||||||
Reference in New Issue
Block a user