diff --git a/app/components/Button.js b/app/components/Button.js
index 6ba2b8b5..fc3b4f64 100644
--- a/app/components/Button.js
+++ b/app/components/Button.js
@@ -66,7 +66,11 @@ const RealButton = styled.button`
&:hover {
- background: ${darken(0.05, props.theme.buttonNeutralBackground)};
+ background: ${
+ props.borderOnHover
+ ? props.theme.buttonNeutralBackground
+ : darken(0.05, props.theme.buttonNeutralBackground)
+ };
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
props.theme.buttonNeutralBorder
} 0 0 0 1px inset;
diff --git a/app/components/DocumentHistory.js b/app/components/DocumentHistory.js
new file mode 100644
index 00000000..264e9d23
--- /dev/null
+++ b/app/components/DocumentHistory.js
@@ -0,0 +1,124 @@
+// @flow
+import { observer } from "mobx-react";
+import { CloseIcon } from "outline-icons";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import { useHistory, useRouteMatch } from "react-router-dom";
+import styled from "styled-components";
+import breakpoint from "styled-components-breakpoint";
+import Event from "models/Event";
+import Button from "components/Button";
+import Empty from "components/Empty";
+import Flex from "components/Flex";
+import PaginatedEventList from "components/PaginatedEventList";
+import Scrollable from "components/Scrollable";
+import useStores from "hooks/useStores";
+import { documentUrl } from "utils/routeHelpers";
+
+const EMPTY_ARRAY = [];
+
+function DocumentHistory() {
+ const { events, documents } = useStores();
+ const { t } = useTranslation();
+ const match = useRouteMatch();
+ const history = useHistory();
+
+ const document = documents.getByUrl(match.params.documentSlug);
+ const eventsInDocument = document
+ ? events.inDocument(document.id)
+ : EMPTY_ARRAY;
+
+ const onCloseHistory = () => {
+ history.push(documentUrl(document));
+ };
+
+ const items = React.useMemo(() => {
+ if (
+ eventsInDocument[0] &&
+ document &&
+ eventsInDocument[0].createdAt !== document.updatedAt
+ ) {
+ eventsInDocument.unshift(
+ new Event({
+ name: "documents.latest_version",
+ documentId: document.id,
+ createdAt: document.updatedAt,
+ actor: document.updatedBy,
+ })
+ );
+ }
+
+ return eventsInDocument;
+ }, [eventsInDocument, document]);
+
+ return (
+
+ {document ? (
+
+
+ {t("History")}
+ }
+ onClick={onCloseHistory}
+ borderOnHover
+ neutral
+ />
+
+
+ {t("Oh weird, there's nothing here")}}
+ />
+
+
+ ) : null}
+
+ );
+}
+
+const Position = styled(Flex)`
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ width: ${(props) => props.theme.sidebarWidth}px;
+`;
+
+const Sidebar = styled(Flex)`
+ display: none;
+ position: relative;
+ flex-shrink: 0;
+ background: ${(props) => props.theme.background};
+ width: ${(props) => props.theme.sidebarWidth}px;
+ border-left: 1px solid ${(props) => props.theme.divider};
+ z-index: 1;
+
+ ${breakpoint("tablet")`
+ display: flex;
+ `};
+`;
+
+const Title = styled(Flex)`
+ font-size: 16px;
+ font-weight: 600;
+ text-align: center;
+ align-items: center;
+ justify-content: flex-start;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ width: 0;
+ flex-grow: 1;
+`;
+
+const Header = styled(Flex)`
+ align-items: center;
+ position: relative;
+ padding: 12px;
+ color: ${(props) => props.theme.text};
+ flex-shrink: 0;
+`;
+
+export default observer(DocumentHistory);
diff --git a/app/components/DocumentHistory/DocumentHistory.js b/app/components/DocumentHistory/DocumentHistory.js
deleted file mode 100644
index 68c8576f..00000000
--- a/app/components/DocumentHistory/DocumentHistory.js
+++ /dev/null
@@ -1,199 +0,0 @@
-// @flow
-import ArrowKeyNavigation from "boundless-arrow-key-navigation";
-import { action, observable } from "mobx";
-import { inject, observer } from "mobx-react";
-import { CloseIcon } from "outline-icons";
-import * as React from "react";
-import { type Match, Redirect, type RouterHistory } from "react-router-dom";
-import { Waypoint } from "react-waypoint";
-import styled from "styled-components";
-
-import breakpoint from "styled-components-breakpoint";
-import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
-import DocumentsStore from "stores/DocumentsStore";
-import RevisionsStore from "stores/RevisionsStore";
-
-import Button from "components/Button";
-import Flex from "components/Flex";
-import PlaceholderList from "components/List/Placeholder";
-import Revision from "./components/Revision";
-import { documentHistoryUrl, documentUrl } from "utils/routeHelpers";
-
-type Props = {
- match: Match,
- documents: DocumentsStore,
- revisions: RevisionsStore,
- history: RouterHistory,
-};
-
-@observer
-class DocumentHistory extends React.Component {
- @observable isLoaded: boolean = false;
- @observable isFetching: boolean = false;
- @observable offset: number = 0;
- @observable allowLoadMore: boolean = true;
- @observable redirectTo: ?string;
-
- async componentDidMount() {
- await this.loadMoreResults();
- this.selectFirstRevision();
- }
-
- fetchResults = async () => {
- this.isFetching = true;
-
- const limit = DEFAULT_PAGINATION_LIMIT;
- const results = await this.props.revisions.fetchPage({
- limit,
- offset: this.offset,
- documentId: this.props.match.params.documentSlug,
- });
-
- 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;
- };
-
- selectFirstRevision = () => {
- if (this.revisions.length) {
- const document = this.props.documents.getByUrl(
- this.props.match.params.documentSlug
- );
- if (!document) return;
-
- this.props.history.replace(
- documentHistoryUrl(document, this.revisions[0].id)
- );
- }
- };
-
- @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();
- };
-
- get revisions() {
- const document = this.props.documents.getByUrl(
- this.props.match.params.documentSlug
- );
- if (!document) return [];
- return this.props.revisions.getDocumentRevisions(document.id);
- }
-
- onCloseHistory = () => {
- const document = this.props.documents.getByUrl(
- this.props.match.params.documentSlug
- );
-
- this.redirectTo = documentUrl(document);
- };
-
- render() {
- const document = this.props.documents.getByUrl(
- this.props.match.params.documentSlug
- );
- const showLoading = (!this.isLoaded && this.isFetching) || !document;
-
- if (this.redirectTo) return ;
-
- return (
-
-
-
- History
- }
- onClick={this.onCloseHistory}
- borderOnHover
- neutral
- />
-
- {showLoading ? (
-
-
-
- ) : (
-
- {this.revisions.map((revision, index) => (
-
- ))}
-
- )}
- {this.allowLoadMore && (
-
- )}
-
-
- );
- }
-}
-
-const Loading = styled.div`
- margin: 0 16px;
-`;
-
-const Wrapper = styled(Flex)`
- position: fixed;
- top: 0;
- right: 0;
- z-index: 1;
- min-width: ${(props) => props.theme.sidebarWidth}px;
- height: 100%;
- overflow-y: auto;
- overscroll-behavior: none;
-`;
-
-const Sidebar = styled(Flex)`
- display: none;
- background: ${(props) => props.theme.background};
- min-width: ${(props) => props.theme.sidebarWidth}px;
- border-left: 1px solid ${(props) => props.theme.divider};
- z-index: 1;
-
- ${breakpoint("tablet")`
- display: flex;
- `};
-`;
-
-const Title = styled(Flex)`
- font-size: 16px;
- font-weight: 600;
- text-align: center;
- align-items: center;
- justify-content: flex-start;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- width: 0;
- flex-grow: 1;
-`;
-
-const Header = styled(Flex)`
- align-items: center;
- position: relative;
- padding: 12px;
- border-bottom: 1px solid ${(props) => props.theme.divider};
- color: ${(props) => props.theme.text};
- flex-shrink: 0;
-`;
-
-export default inject("documents", "revisions")(DocumentHistory);
diff --git a/app/components/DocumentHistory/components/Revision.js b/app/components/DocumentHistory/components/Revision.js
deleted file mode 100644
index f1469f89..00000000
--- a/app/components/DocumentHistory/components/Revision.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// @flow
-import { format } from "date-fns";
-import * as React from "react";
-import { NavLink } from "react-router-dom";
-import styled, { withTheme } from "styled-components";
-
-import Document from "models/Document";
-import Revision from "models/Revision";
-import Avatar from "components/Avatar";
-import Flex from "components/Flex";
-import Time from "components/Time";
-import RevisionMenu from "menus/RevisionMenu";
-import { type Theme } from "types";
-
-import { documentHistoryUrl } from "utils/routeHelpers";
-
-type Props = {
- theme: Theme,
- showMenu: boolean,
- selected: boolean,
- document: Document,
- revision: Revision,
-};
-
-class RevisionListItem extends React.Component {
- render() {
- const { revision, document, showMenu, selected, theme } = this.props;
-
- return (
-
-
- {" "}
- {revision.createdBy.name}
-
-
-
-
- {showMenu && (
-
- )}
-
- );
- }
-}
-
-const StyledAvatar = styled(Avatar)`
- border-color: transparent;
- margin-right: 4px;
-`;
-
-const StyledRevisionMenu = styled(RevisionMenu)`
- position: absolute;
- right: 16px;
- top: 20px;
-`;
-
-const StyledNavLink = styled(NavLink)`
- color: ${(props) => props.theme.text};
- display: block;
- padding: 8px 16px;
- font-size: 15px;
- position: relative;
-`;
-
-const Author = styled(Flex)`
- font-weight: 500;
- padding: 0;
- margin: 0;
-`;
-
-const Meta = styled.p`
- font-size: 14px;
- opacity: 0.75;
- margin: 0 0 2px;
- padding: 0;
-`;
-
-export default withTheme(RevisionListItem);
diff --git a/app/components/DocumentHistory/index.js b/app/components/DocumentHistory/index.js
deleted file mode 100644
index 7d566709..00000000
--- a/app/components/DocumentHistory/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// @flow
-import DocumentHistory from "./DocumentHistory";
-export default DocumentHistory;
diff --git a/app/components/EventListItem.js b/app/components/EventListItem.js
new file mode 100644
index 00000000..47f2d296
--- /dev/null
+++ b/app/components/EventListItem.js
@@ -0,0 +1,163 @@
+// @flow
+import {
+ TrashIcon,
+ ArchiveIcon,
+ EditIcon,
+ PublishIcon,
+ MoveIcon,
+ CheckboxIcon,
+} from "outline-icons";
+import * as React from "react";
+import { useTranslation } from "react-i18next";
+import styled from "styled-components";
+import Document from "models/Document";
+import Event from "models/Event";
+import Avatar from "components/Avatar";
+import Item, { Actions } from "components/List/Item";
+import Time from "components/Time";
+import RevisionMenu from "menus/RevisionMenu";
+import { documentHistoryUrl } from "utils/routeHelpers";
+
+type Props = {|
+ document: Document,
+ event: Event,
+ latest?: boolean,
+|};
+
+const EventListItem = ({ event, latest, document }: Props) => {
+ const { t } = useTranslation();
+ const opts = { userName: event.actor.name };
+ const isRevision = event.name === "revisions.create";
+ let meta, icon, to;
+
+ switch (event.name) {
+ case "revisions.create":
+ case "documents.latest_version": {
+ if (latest) {
+ icon = ;
+ meta = t("Latest version");
+ to = documentHistoryUrl(document);
+ break;
+ } else {
+ icon = ;
+ meta = t("{{userName}} edited", opts);
+ to = documentHistoryUrl(document, event.modelId || "");
+ break;
+ }
+ }
+ case "documents.archive":
+ icon = ;
+ meta = t("{{userName}} archived", opts);
+ break;
+ case "documents.unarchive":
+ meta = t("{{userName}} restored", opts);
+ break;
+ case "documents.delete":
+ icon = ;
+ meta = t("{{userName}} deleted", opts);
+ break;
+ case "documents.restore":
+ meta = t("{{userName}} moved from trash", opts);
+ break;
+ case "documents.publish":
+ icon = ;
+ meta = t("{{userName}} published", opts);
+ break;
+ case "documents.move":
+ icon = ;
+ meta = t("{{userName}} moved", opts);
+ break;
+ default:
+ console.warn("Unhandled event: ", event.name);
+ }
+
+ if (!meta) {
+ return null;
+ }
+
+ return (
+
+ }
+ image={}
+ subtitle={
+
+ {icon}
+ {meta}
+
+ }
+ actions={
+ isRevision ? (
+
+ ) : undefined
+ }
+ />
+ );
+};
+
+const Subtitle = styled.span`
+ svg {
+ margin: -3px;
+ margin-right: 2px;
+ }
+`;
+
+const ListItem = styled(Item)`
+ border: 0;
+ position: relative;
+ margin: 8px;
+ padding: 8px;
+ border-radius: 8px;
+
+ img {
+ border-color: transparent;
+ }
+
+ &::before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: -4px;
+ left: 23px;
+ width: 2px;
+ height: calc(100% + 8px);
+ background: ${(props) => props.theme.textSecondary};
+ opacity: 0.25;
+ }
+
+ &:nth-child(2)::before {
+ height: 50%;
+ top: 50%;
+ }
+
+ &:last-child::before {
+ height: 50%;
+ }
+
+ &:first-child:last-child::before {
+ display: none;
+ }
+
+ ${Actions} {
+ opacity: 0.25;
+ transition: opacity 100ms ease-in-out;
+ }
+
+ &:hover {
+ ${Actions} {
+ opacity: 1;
+ }
+ }
+`;
+
+export default EventListItem;
diff --git a/app/components/List/Item.js b/app/components/List/Item.js
index 29b300f8..0dfc5f0b 100644
--- a/app/components/List/Item.js
+++ b/app/components/List/Item.js
@@ -1,41 +1,62 @@
// @flow
import * as React from "react";
-import styled from "styled-components";
+import styled, { useTheme } from "styled-components";
import Flex from "components/Flex";
+import NavLink from "components/NavLink";
-type Props = {
+type Props = {|
image?: React.Node,
+ to?: string,
title: React.Node,
subtitle?: React.Node,
actions?: React.Node,
border?: boolean,
small?: boolean,
-};
+|};
-const ListItem = ({
- image,
- title,
- subtitle,
- actions,
- small,
- border,
-}: Props) => {
+const ListItem = (
+ { image, title, subtitle, actions, small, border, to, ...rest }: Props,
+ ref
+) => {
+ const theme = useTheme();
const compact = !subtitle;
- return (
-
+ const content = (selected) => (
+ <>
{image && {image}}
-
+
{title}
- {subtitle && {subtitle}}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
- {actions && {actions}}
+ {actions && {actions}}
+ >
+ );
+
+ return (
+
+ {to ? content : content(false)}
);
};
-const Wrapper = styled.li`
+const Wrapper = styled.div`
display: flex;
+ user-select: none;
padding: ${(props) => (props.$border === false ? 0 : "8px 0")};
margin: ${(props) => (props.$border === false ? "8px 0" : 0)};
border-bottom: 1px solid
@@ -57,28 +78,36 @@ const Image = styled(Flex)`
`;
const Heading = styled.p`
- font-size: ${(props) => (props.$small ? 15 : 16)}px;
+ font-size: ${(props) => (props.$small ? 14 : 16)}px;
font-weight: 500;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
- line-height: 1.2;
+ line-height: ${(props) => (props.$small ? 1.3 : 1.2)};
margin: 0;
`;
-const Content = styled(Flex)`
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
flex-grow: 1;
+ color: ${(props) => (props.$selected ? props.theme.white : props.theme.text)};
`;
const Subtitle = styled.p`
margin: 0;
font-size: ${(props) => (props.$small ? 13 : 14)}px;
- color: ${(props) => props.theme.textTertiary};
+ color: ${(props) =>
+ props.$selected ? props.theme.white50 : props.theme.textTertiary};
margin-top: -2px;
`;
-const Actions = styled.div`
+export const Actions = styled(Flex)`
align-self: center;
+ justify-content: center;
+ margin-right: 4px;
+ color: ${(props) =>
+ props.$selected ? props.theme.white : props.theme.textSecondary};
`;
-export default ListItem;
+export default React.forwardRef(ListItem);
diff --git a/app/components/LocaleTime.js b/app/components/LocaleTime.js
index 107a7c21..b01094f5 100644
--- a/app/components/LocaleTime.js
+++ b/app/components/LocaleTime.js
@@ -1,5 +1,5 @@
// @flow
-import { format, formatDistanceToNow } from "date-fns";
+import { format as formatDate, formatDistanceToNow } from "date-fns";
import {
enUS,
de,
@@ -57,6 +57,9 @@ type Props = {
tooltipDelay?: number,
addSuffix?: boolean,
shorten?: boolean,
+ relative?: boolean,
+ format?: string,
+ tooltip?: boolean,
};
function LocaleTime({
@@ -64,7 +67,10 @@ function LocaleTime({
children,
dateTime,
shorten,
+ format,
+ relative,
tooltipDelay,
+ tooltip,
}: Props) {
const userLocale = useUserLocale();
const [_, setMinutesMounted] = React.useState(0); // eslint-disable-line no-unused-vars
@@ -82,25 +88,34 @@ function LocaleTime({
};
}, []);
- let content = formatDistanceToNow(Date.parse(dateTime), {
+ const locale = userLocale ? locales[userLocale] : undefined;
+ let relativeContent = formatDistanceToNow(Date.parse(dateTime), {
addSuffix,
- locale: userLocale ? locales[userLocale] : undefined,
+ locale,
});
if (shorten) {
- content = content
+ relativeContent = relativeContent
.replace("about", "")
.replace("less than a minute ago", "just now")
.replace("minute", "min");
}
+ const tooltipContent = formatDate(
+ Date.parse(dateTime),
+ format || "MMMM do, yyyy h:mm a",
+ { locale }
+ );
+
+ const content = children || relative ? relativeContent : tooltipContent;
+
+ if (!tooltip) {
+ return content;
+ }
+
return (
-
-
+
+
);
}
diff --git a/app/components/NavLink.js b/app/components/NavLink.js
new file mode 100644
index 00000000..12dbc8f9
--- /dev/null
+++ b/app/components/NavLink.js
@@ -0,0 +1,26 @@
+// @flow
+import * as React from "react";
+import { NavLink, Route, type Match } from "react-router-dom";
+
+type Props = {
+ children?: (match: Match) => React.Node,
+ exact?: boolean,
+ to: string,
+};
+
+export default function NavLinkWithChildrenFunc({
+ to,
+ exact = false,
+ children,
+ ...rest
+}: Props) {
+ return (
+
+ {({ match }) => (
+
+ {children ? children(match) : null}
+
+ )}
+
+ );
+}
diff --git a/app/components/NudeButton.js b/app/components/NudeButton.js
index 5e0027b2..193f56a8 100644
--- a/app/components/NudeButton.js
+++ b/app/components/NudeButton.js
@@ -12,6 +12,7 @@ const Button = styled.button`
padding: 0;
cursor: pointer;
user-select: none;
+ color: inherit;
`;
export default React.forwardRef(
diff --git a/app/components/PaginatedEventList.js b/app/components/PaginatedEventList.js
new file mode 100644
index 00000000..5414c49c
--- /dev/null
+++ b/app/components/PaginatedEventList.js
@@ -0,0 +1,53 @@
+// @flow
+import * as React from "react";
+import styled from "styled-components";
+import Document from "models/Document";
+import Event from "models/Event";
+import PaginatedList from "components/PaginatedList";
+import EventListItem from "./EventListItem";
+
+type Props = {|
+ events: Event[],
+ document: Document,
+ fetch: (options: ?Object) => Promise,
+ options?: Object,
+ heading?: React.Node,
+ empty?: React.Node,
+|};
+
+const PaginatedEventList = React.memo(function PaginatedEventList({
+ empty,
+ heading,
+ events,
+ fetch,
+ options,
+ document,
+ ...rest
+}: Props) {
+ return (
+ (
+
+ )}
+ renderHeading={(name) => {name}}
+ />
+ );
+});
+
+const Heading = styled("h3")`
+ font-size: 14px;
+ padding: 0 12px;
+`;
+
+export default PaginatedEventList;
diff --git a/app/components/PaginatedList.js b/app/components/PaginatedList.js
index 7deeb9f5..ca1d7061 100644
--- a/app/components/PaginatedList.js
+++ b/app/components/PaginatedList.js
@@ -4,10 +4,12 @@ import { isEqual } from "lodash";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
+import { withTranslation, type TFunction } from "react-i18next";
import { Waypoint } from "react-waypoint";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import DelayedMount from "components/DelayedMount";
import PlaceholderList from "components/List/Placeholder";
+import { dateToHeading } from "utils/dates";
type Props = {
fetch?: (options: ?Object) => Promise,
@@ -15,7 +17,9 @@ type Props = {
heading?: React.Node,
empty?: React.Node,
items: any[],
- renderItem: (any) => React.Node,
+ renderItem: (any, index: number) => React.Node,
+ renderHeading?: (name: React.Element | string) => React.Node,
+ t: TFunction,
};
@observer
@@ -101,8 +105,9 @@ class PaginatedList extends React.Component {
};
render() {
- const { items, heading, empty } = this.props;
+ const { items, heading, empty, renderHeading } = this.props;
+ let previousHeading = "";
const showLoading =
this.isFetching && !this.isFetchingMore && !this.isInitiallyLoaded;
const showEmpty = !items.length && !showLoading;
@@ -119,7 +124,37 @@ class PaginatedList extends React.Component {
mode={ArrowKeyNavigation.mode.VERTICAL}
defaultActiveChildIndex={0}
>
- {items.slice(0, this.renderCount).map(this.props.renderItem)}
+ {items.slice(0, this.renderCount).map((item, index) => {
+ const children = this.props.renderItem(item, index);
+
+ // If there is no renderHeading method passed then no date
+ // headings are rendered
+ if (!renderHeading) {
+ return children;
+ }
+
+ // Our models have standard date fields, updatedAt > createdAt.
+ // Get what a heading would look like for this item
+ const currentDate =
+ item.updatedAt || item.createdAt || previousHeading;
+ const currentHeading = dateToHeading(currentDate, this.props.t);
+
+ // If the heading is different to any previous heading then we
+ // should render it, otherwise the item can go under the previous
+ // heading
+ if (!previousHeading || currentHeading !== previousHeading) {
+ previousHeading = currentHeading;
+
+ return (
+
+ {renderHeading(currentHeading)}
+ {children}
+
+ );
+ }
+
+ return children;
+ })}
{this.allowLoadMore && (
@@ -136,4 +171,6 @@ class PaginatedList extends React.Component {
}
}
-export default PaginatedList;
+export const Component = PaginatedList;
+
+export default withTranslation()(PaginatedList);
diff --git a/app/components/PaginatedList.test.js b/app/components/PaginatedList.test.js
index 506c9c54..438f65ac 100644
--- a/app/components/PaginatedList.test.js
+++ b/app/components/PaginatedList.test.js
@@ -4,7 +4,7 @@ import { shallow } from "enzyme";
import * as React from "react";
import { DEFAULT_PAGINATION_LIMIT } from "stores/BaseStore";
import { runAllPromises } from "../test/support";
-import PaginatedList from "./PaginatedList";
+import { Component as PaginatedList } from "./PaginatedList";
describe("PaginatedList", () => {
const render = () => null;
diff --git a/app/components/Tab.js b/app/components/Tab.js
index feb1c0df..4ad6a4cd 100644
--- a/app/components/Tab.js
+++ b/app/components/Tab.js
@@ -1,25 +1,13 @@
// @flow
import { m } from "framer-motion";
import * as React from "react";
-import { NavLink, Route } from "react-router-dom";
-import styled, { withTheme } from "styled-components";
-import { type Theme } from "types";
+import styled, { useTheme } from "styled-components";
+import NavLinkWithChildrenFunc from "components/NavLink";
type Props = {
- theme: Theme,
children: React.Node,
};
-const NavLinkWithChildrenFunc = ({ to, exact = false, children, ...rest }) => (
-
- {({ match }) => (
-
- {children(match)}
-
- )}
-
-);
-
const TabLink = styled(NavLinkWithChildrenFunc)`
position: relative;
display: inline-flex;
@@ -53,7 +41,8 @@ const transition = {
damping: 30,
};
-function Tab({ theme, children, ...rest }: Props) {
+export default function Tab({ children, ...rest }: Props) {
+ const theme = useTheme();
const activeStyle = {
color: theme.textSecondary,
};
@@ -75,5 +64,3 @@ function Tab({ theme, children, ...rest }: Props) {
);
}
-
-export default withTheme(Tab);
diff --git a/app/components/Time.js b/app/components/Time.js
index 8569de78..78dd550a 100644
--- a/app/components/Time.js
+++ b/app/components/Time.js
@@ -11,6 +11,7 @@ type Props = {
children?: React.Node,
tooltipDelay?: number,
addSuffix?: boolean,
+ format?: string,
shorten?: boolean,
};
diff --git a/app/menus/RevisionMenu.js b/app/menus/RevisionMenu.js
index 000f8099..1da8e43f 100644
--- a/app/menus/RevisionMenu.js
+++ b/app/menus/RevisionMenu.js
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useMenuState } from "reakit/Menu";
import Document from "models/Document";
-import Revision from "models/Revision";
import ContextMenu from "components/ContextMenu";
import MenuItem from "components/ContextMenu/MenuItem";
import OverflowMenuButton from "components/ContextMenu/OverflowMenuButton";
@@ -16,12 +15,11 @@ import { documentHistoryUrl } from "utils/routeHelpers";
type Props = {|
document: Document,
- revision: Revision,
- iconColor?: string,
+ revisionId: string,
className?: string,
|};
-function RevisionMenu({ document, revision, className, iconColor }: Props) {
+function RevisionMenu({ document, revisionId, className }: Props) {
const { showToast } = useToasts();
const menu = useMenuState({ modal: true });
const { t } = useTranslation();
@@ -30,11 +28,11 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) {
const handleRestore = React.useCallback(
async (ev: SyntheticEvent<>) => {
ev.preventDefault();
- await document.restore({ revisionId: revision.id });
+ await document.restore({ revisionId });
showToast(t("Document restored"), { type: "success" });
history.push(document.url);
},
- [history, showToast, t, document, revision]
+ [history, showToast, t, document, revisionId]
);
const handleCopy = React.useCallback(() => {
@@ -43,14 +41,14 @@ function RevisionMenu({ document, revision, className, iconColor }: Props) {
const url = `${window.location.origin}${documentHistoryUrl(
document,
- revision.id
+ revisionId
)}`;
return (
<>
diff --git a/app/models/Event.js b/app/models/Event.js
index d9e69b0d..f3d5e997 100644
--- a/app/models/Event.js
+++ b/app/models/Event.js
@@ -20,20 +20,6 @@ class Event extends BaseModel {
published: boolean,
templateId: string,
};
-
- get model() {
- return this.name.split(".")[0];
- }
-
- get verb() {
- return this.name.split(".")[1];
- }
-
- get verbPastTense() {
- const v = this.verb;
- if (v.endsWith("e")) return `${v}d`;
- return `${v}ed`;
- }
}
export default Event;
diff --git a/app/stores/EventsStore.js b/app/stores/EventsStore.js
new file mode 100644
index 00000000..4f0dcbec
--- /dev/null
+++ b/app/stores/EventsStore.js
@@ -0,0 +1,23 @@
+// @flow
+import { sortBy, filter } from "lodash";
+import { computed } from "mobx";
+import Event from "models/Event";
+import BaseStore from "./BaseStore";
+import RootStore from "./RootStore";
+
+export default class EventsStore extends BaseStore {
+ actions = ["list"];
+
+ constructor(rootStore: RootStore) {
+ super(rootStore, Event);
+ }
+
+ @computed
+ get orderedData(): Event[] {
+ return sortBy(Array.from(this.data.values()), "createdAt").reverse();
+ }
+
+ inDocument(documentId: string): Event[] {
+ return filter(this.orderedData, (event) => event.documentId === documentId);
+ }
+}
diff --git a/app/stores/RootStore.js b/app/stores/RootStore.js
index 48718686..13ebf28c 100644
--- a/app/stores/RootStore.js
+++ b/app/stores/RootStore.js
@@ -5,6 +5,7 @@ import CollectionGroupMembershipsStore from "./CollectionGroupMembershipsStore";
import CollectionsStore from "./CollectionsStore";
import DocumentPresenceStore from "./DocumentPresenceStore";
import DocumentsStore from "./DocumentsStore";
+import EventsStore from "./EventsStore";
import GroupMembershipsStore from "./GroupMembershipsStore";
import GroupsStore from "./GroupsStore";
import IntegrationsStore from "./IntegrationsStore";
@@ -24,6 +25,7 @@ export default class RootStore {
collections: CollectionsStore;
collectionGroupMemberships: CollectionGroupMembershipsStore;
documents: DocumentsStore;
+ events: EventsStore;
groups: GroupsStore;
groupMemberships: GroupMembershipsStore;
integrations: IntegrationsStore;
@@ -46,6 +48,7 @@ export default class RootStore {
this.collections = new CollectionsStore(this);
this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this);
this.documents = new DocumentsStore(this);
+ this.events = new EventsStore(this);
this.groups = new GroupsStore(this);
this.groupMemberships = new GroupMembershipsStore(this);
this.integrations = new IntegrationsStore(this);
@@ -66,6 +69,7 @@ export default class RootStore {
this.collections.clear();
this.collectionGroupMemberships.clear();
this.documents.clear();
+ this.events.clear();
this.groups.clear();
this.groupMemberships.clear();
this.integrations.clear();
diff --git a/app/test/setup.js b/app/test/setup.js
index bfed90c9..23ce7ef7 100644
--- a/app/test/setup.js
+++ b/app/test/setup.js
@@ -3,6 +3,9 @@
import localStorage from '../../__mocks__/localStorage';
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
+import { initI18n } from "shared/i18n";
+
+initI18n();
Enzyme.configure({ adapter: new Adapter() });
diff --git a/app/utils/dates.js b/app/utils/dates.js
new file mode 100644
index 00000000..a902f475
--- /dev/null
+++ b/app/utils/dates.js
@@ -0,0 +1,51 @@
+// @flow
+import {
+ isToday,
+ isYesterday,
+ differenceInCalendarWeeks,
+ differenceInCalendarMonths,
+} from "date-fns";
+import * as React from "react";
+import { type TFunction } from "react-i18next";
+import LocaleTime from "components/LocaleTime";
+
+export function dateToHeading(dateTime: string, t: TFunction) {
+ const date = Date.parse(dateTime);
+ const now = new Date();
+
+ if (isToday(date)) {
+ return t("Today");
+ }
+
+ if (isYesterday(date)) {
+ return t("Yesterday");
+ }
+
+ // If the current calendar week but not today or yesterday then return the day
+ // of the week as a string. We use the LocaleTime component here to gain
+ // async bundle loading of languages
+ const weekDiff = differenceInCalendarWeeks(now, date);
+ if (weekDiff === 0) {
+ return ;
+ }
+
+ if (weekDiff === 1) {
+ return t("Last week");
+ }
+
+ const monthDiff = differenceInCalendarMonths(now, date);
+ if (monthDiff === 0) {
+ return t("This month");
+ }
+
+ if (monthDiff === 1) {
+ return t("Last month");
+ }
+
+ if (monthDiff <= 12) {
+ return t("This year");
+ }
+
+ // If older than the current calendar year then just print the year e.g 2020
+ return ;
+}
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 9a349482..2012361a 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -14,6 +14,8 @@
"Drafts": "Drafts",
"Templates": "Templates",
"Deleted Collection": "Deleted Collection",
+ "History": "History",
+ "Oh weird, there's nothing here": "Oh weird, there's nothing here",
"New": "New",
"Only visible to you": "Only visible to you",
"Draft": "Draft",
@@ -110,6 +112,14 @@
"our engineers have been notified": "our engineers have been notified",
"Report a Bug": "Report a Bug",
"Show Detail": "Show Detail",
+ "Latest version": "Latest version",
+ "{{userName}} edited": "{{userName}} edited",
+ "{{userName}} archived": "{{userName}} archived",
+ "{{userName}} restored": "{{userName}} restored",
+ "{{userName}} deleted": "{{userName}} deleted",
+ "{{userName}} moved from trash": "{{userName}} moved from trash",
+ "{{userName}} published": "{{userName}} published",
+ "{{userName}} moved": "{{userName}} moved",
"Icon": "Icon",
"Show menu": "Show menu",
"Choose icon": "Choose icon",
@@ -200,7 +210,6 @@
"Unpublish": "Unpublish",
"Permanently delete": "Permanently delete",
"Move": "Move",
- "History": "History",
"Download": "Download",
"Print": "Print",
"Move {{ documentName }}": "Move {{ documentName }}",
@@ -552,5 +561,11 @@
"Joined": "Joined",
"{{ time }} ago.": "{{ time }} ago.",
"Edit Profile": "Edit Profile",
- "{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet."
+ "{{ userName }} hasn’t updated any documents yet.": "{{ userName }} hasn’t updated any documents yet.",
+ "Today": "Today",
+ "Yesterday": "Yesterday",
+ "Last week": "Last week",
+ "This month": "This month",
+ "Last month": "Last month",
+ "This year": "This year"
}