diff --git a/app/models/Share.js b/app/models/Share.js
index 3c5305552..c290271a3 100644
--- a/app/models/Share.js
+++ b/app/models/Share.js
@@ -9,6 +9,7 @@ class Share extends BaseModel {
documentId: string;
documentTitle: string;
documentUrl: string;
+ lastAccessedAt: ?string;
createdBy: User;
createdAt: string;
updatedAt: string;
diff --git a/app/scenes/Settings/Shares.js b/app/scenes/Settings/Shares.js
index eaceaa978..b606d56bb 100644
--- a/app/scenes/Settings/Shares.js
+++ b/app/scenes/Settings/Shares.js
@@ -1,12 +1,13 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
+import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import CenteredContent from "components/CenteredContent";
import Empty from "components/Empty";
import HelpText from "components/HelpText";
-import List from "components/List";
import PageTitle from "components/PageTitle";
+import PaginatedList from "components/PaginatedList";
import Subheading from "components/Subheading";
import ShareListItem from "./components/ShareListItem";
import useCurrentTeam from "hooks/useCurrentTeam";
@@ -14,43 +15,40 @@ import useStores from "hooks/useStores";
function Shares() {
const team = useCurrentTeam();
+ const { t } = useTranslation();
const { shares, auth, policies } = useStores();
const canShareDocuments = auth.team && auth.team.sharing;
- const hasSharedDocuments = shares.orderedData.length > 0;
const can = policies.abilities(team.id);
- React.useEffect(() => {
- shares.fetchPage({ limit: 100 });
- }, [shares]);
-
return (
-
-
Share Links
+
+
{t("Share Links")}
- Documents that have been shared are listed below. Anyone that has the
- public link can access a read-only version of the document until the
- link has been revoked.
+
+ Documents that have been shared are listed below. Anyone that has the
+ public link can access a read-only version of the document until the
+ link has been revoked.
+
{can.manage && (
{!canShareDocuments && (
- Sharing is currently disabled.
+ {t("Sharing is currently disabled.")}
)}{" "}
- You can turn {canShareDocuments ? "off" : "on"} public document
- sharing in security settings.
+ }}
+ />
)}
- Shared Documents
- {hasSharedDocuments ? (
-
- {shares.published.map((share) => (
-
- ))}
-
- ) : (
- No share links, yet.
- )}
+ {t("Shared documents")}
+ {t("No share links, yet.")}}
+ fetch={shares.fetchPage}
+ renderItem={(item) => }
+ />
);
}
diff --git a/app/scenes/Settings/components/ShareListItem.js b/app/scenes/Settings/components/ShareListItem.js
index 78cb7b5a1..d948b5179 100644
--- a/app/scenes/Settings/components/ShareListItem.js
+++ b/app/scenes/Settings/components/ShareListItem.js
@@ -1,23 +1,33 @@
// @flow
import * as React from "react";
+import { useTranslation } from "react-i18next";
import Share from "models/Share";
import ListItem from "components/List/Item";
import Time from "components/Time";
import ShareMenu from "menus/ShareMenu";
-type Props = {
+type Props = {|
share: Share,
-};
+|};
const ShareListItem = ({ share }: Props) => {
+ const { t } = useTranslation();
+ const { lastAccessedAt } = share;
+
return (
- Shared ago by{" "}
- {share.createdBy.name}
+ {t("Shared")} {" "}
+ {t("by {{ name }}", { name: share.createdBy.name })}{" "}
+ {lastAccessedAt && (
+ <>
+ {" "}
+ · {t("Last accessed")}{" "}
+
+ >
+ )}
>
}
actions={}
diff --git a/server/api/documents.js b/server/api/documents.js
index 1944b8d1b..359420b85 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -518,6 +518,8 @@ async function loadDocument({ id, shareId, user }) {
if (!team.sharing) {
throw new AuthorizationError();
}
+
+ await share.update({ lastAccessedAt: new Date() });
} else {
document = await Document.findByPk(id, {
userId: user ? user.id : undefined,
diff --git a/server/api/documents.test.js b/server/api/documents.test.js
index e0ce821f7..026856d99 100644
--- a/server/api/documents.test.js
+++ b/server/api/documents.test.js
@@ -93,6 +93,9 @@ describe("#documents.info", () => {
expect(body.data.id).toEqual(document.id);
expect(body.data.createdBy).toEqual(undefined);
expect(body.data.updatedBy).toEqual(undefined);
+
+ await share.reload();
+ expect(share.lastAccessedAt).toBeTruthy();
});
it("should not return document from shareId if sharing is disabled for team", async () => {
diff --git a/server/api/shares.js b/server/api/shares.js
index 4123ae50d..11ac62b25 100644
--- a/server/api/shares.js
+++ b/server/api/shares.js
@@ -36,7 +36,7 @@ router.post("shares.info", auth(), async (ctx) => {
authorize(user, "read", share);
ctx.body = {
- data: presentShare(share),
+ data: presentShare(share, user.isAdmin),
policies: presentPolicies(user, [share]),
};
});
@@ -89,7 +89,7 @@ router.post("shares.list", auth(), pagination(), async (ctx) => {
ctx.body = {
pagination: ctx.state.pagination,
- data: shares.map(presentShare),
+ data: shares.map((share) => presentShare(share, user.isAdmin)),
policies: presentPolicies(user, shares),
};
});
@@ -117,7 +117,7 @@ router.post("shares.update", auth(), async (ctx) => {
});
ctx.body = {
- data: presentShare(share),
+ data: presentShare(share, user.isAdmin),
policies: presentPolicies(user, [share]),
};
});
diff --git a/server/migrations/20210418053152-share-last-viewed.js b/server/migrations/20210418053152-share-last-viewed.js
new file mode 100644
index 000000000..c84076bcf
--- /dev/null
+++ b/server/migrations/20210418053152-share-last-viewed.js
@@ -0,0 +1,14 @@
+'use strict';
+
+module.exports = {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.addColumn("shares", "lastAccessedAt", {
+ type: Sequelize.DATE,
+ allowNull: true,
+ });
+ },
+
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.removeColumn("shares", "lastAccessedAt");
+ }
+};
diff --git a/server/models/Share.js b/server/models/Share.js
index ae28f40c0..9adefde54 100644
--- a/server/models/Share.js
+++ b/server/models/Share.js
@@ -12,6 +12,7 @@ const Share = sequelize.define(
published: DataTypes.BOOLEAN,
revokedAt: DataTypes.DATE,
revokedById: DataTypes.UUID,
+ lastAccessedAt: DataTypes.DATE,
},
{
getterMethods: {
diff --git a/server/presenters/event.js b/server/presenters/event.js
index 32fafbb59..78dcf90cc 100644
--- a/server/presenters/event.js
+++ b/server/presenters/event.js
@@ -2,7 +2,7 @@
import { Event } from "../models";
import presentUser from "./user";
-export default function present(event: Event, auditLog: boolean = false) {
+export default function present(event: Event, isAdmin: boolean = false) {
let data = {
id: event.id,
name: event.name,
@@ -16,7 +16,7 @@ export default function present(event: Event, auditLog: boolean = false) {
actor: presentUser(event.actor),
};
- if (!auditLog) {
+ if (!isAdmin) {
delete data.actorIpAddress;
}
diff --git a/server/presenters/share.js b/server/presenters/share.js
index a33000e06..4d2ae962f 100644
--- a/server/presenters/share.js
+++ b/server/presenters/share.js
@@ -2,8 +2,8 @@
import { Share } from "../models";
import { presentUser } from ".";
-export default function present(share: Share) {
- return {
+export default function present(share: Share, isAdmin: boolean = false) {
+ let data = {
id: share.id,
documentId: share.documentId,
documentTitle: share.document.title,
@@ -11,7 +11,14 @@ export default function present(share: Share) {
published: share.published,
url: `${share.team.url}/share/${share.id}`,
createdBy: presentUser(share.user),
+ lastAccessedAt: share.lastAccessedAt,
createdAt: share.createdAt,
updatedAt: share.updatedAt,
};
+
+ if (!isAdmin) {
+ delete data.lastAccessedAt;
+ }
+
+ return data;
}
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 2d703d1e1..60f272775 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -345,6 +345,9 @@
"No documents found for your search filters. <1>1>": "No documents found for your search filters. <1>1>",
"Create a new document?": "Create a new document?",
"Clear filters": "Clear filters",
+ "Shared": "Shared",
+ "by {{ name }}": "by {{ name }}",
+ "Last accessed": "Last accessed",
"Import started": "Import started",
"Export in progress…": "Export in progress…",
"It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.": "It is possible to import a zip file of folders and Markdown files previously exported from an Outline instance. Support will soon be added for importing from other services.",
@@ -376,6 +379,11 @@
"Delete Account": "Delete Account",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
"Delete account": "Delete account",
+ "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
+ "Sharing is currently disabled.": "Sharing is currently disabled.",
+ "You can globally enable and disable public document sharing in the security settings.": "You can globally enable and disable public document sharing in the security settings.",
+ "Shared documents": "Shared documents",
+ "No share links, yet.": "No share links, yet.",
"You’ve not starred any documents yet.": "You’ve not starred any documents yet.",
"There are no templates just yet.": "There are no templates just yet.",
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",