Display import source data on documents (#6651)
* Display import source in Insights * Ensure sourceMetadata is not returned on public requests * Support createdByName * Prefer display source name
This commit is contained in:
@@ -96,7 +96,16 @@ const DocumentMeta: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (createdAt === updatedAt) {
|
} else if (createdAt === updatedAt) {
|
||||||
content = (
|
content = document.sourceMetadata ? (
|
||||||
|
<span>
|
||||||
|
{document.sourceMetadata.createdByName
|
||||||
|
? t("{{ userName }} created", {
|
||||||
|
userName: document.sourceMetadata.createdByName,
|
||||||
|
})
|
||||||
|
: t("Imported")}{" "}
|
||||||
|
<Time dateTime={createdAt} addSuffix />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span>
|
<span>
|
||||||
{lastUpdatedByCurrentUser
|
{lastUpdatedByCurrentUser
|
||||||
? t("You created")
|
? t("You created")
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { addDays, differenceInDays } from "date-fns";
|
import { addDays, differenceInDays } from "date-fns";
|
||||||
import i18n, { t } from "i18next";
|
import i18n, { t } from "i18next";
|
||||||
|
import capitalize from "lodash/capitalize";
|
||||||
import floor from "lodash/floor";
|
import floor from "lodash/floor";
|
||||||
import { action, autorun, computed, observable, set } from "mobx";
|
import { action, autorun, computed, observable, set } from "mobx";
|
||||||
import { ExportContentType, NotificationEventType } from "@shared/types";
|
import {
|
||||||
|
ExportContentType,
|
||||||
|
FileOperationFormat,
|
||||||
|
NotificationEventType,
|
||||||
|
} from "@shared/types";
|
||||||
import type { JSONObject, NavigationNode } from "@shared/types";
|
import type { JSONObject, NavigationNode } from "@shared/types";
|
||||||
import Storage from "@shared/utils/Storage";
|
import Storage from "@shared/utils/Storage";
|
||||||
import { isRTL } from "@shared/utils/rtl";
|
import { isRTL } from "@shared/utils/rtl";
|
||||||
@@ -56,6 +61,43 @@ export default class Document extends ParanoidModel {
|
|||||||
@observable
|
@observable
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original data source of the document, if imported.
|
||||||
|
*/
|
||||||
|
sourceMetadata?: {
|
||||||
|
/**
|
||||||
|
* The type of importer that was used, if any. This can also be empty if an individual file was
|
||||||
|
* imported through drag-and-drop, for example.
|
||||||
|
*/
|
||||||
|
importType?: FileOperationFormat;
|
||||||
|
/** The date this document was imported. */
|
||||||
|
importedAt?: string;
|
||||||
|
/** The name of the user the created the original source document. */
|
||||||
|
createdByName?: string;
|
||||||
|
/** The name of the file this document was imported from. */
|
||||||
|
fileName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the original data source, if imported.
|
||||||
|
*/
|
||||||
|
get sourceName() {
|
||||||
|
if (!this.sourceMetadata) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.sourceMetadata.importType) {
|
||||||
|
case FileOperationFormat.MarkdownZip:
|
||||||
|
return "Markdown";
|
||||||
|
case FileOperationFormat.JSON:
|
||||||
|
return "JSON";
|
||||||
|
case FileOperationFormat.Notion:
|
||||||
|
return "Notion";
|
||||||
|
default:
|
||||||
|
return capitalize(this.sourceMetadata.importType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the collection that this document belongs to, if any.
|
* The id of the collection that this document belongs to, if any.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
import { useHistory, useRouteMatch } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { s } from "@shared/styles";
|
import { s } from "@shared/styles";
|
||||||
|
import { stringToColor } from "@shared/utils/color";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import { useDocumentContext } from "~/components/DocumentContext";
|
import { useDocumentContext } from "~/components/DocumentContext";
|
||||||
@@ -56,6 +57,20 @@ function Insights() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Content column>
|
<Content column>
|
||||||
|
{document.sourceMetadata && (
|
||||||
|
<>
|
||||||
|
<Heading>{t("Source")}</Heading>
|
||||||
|
{
|
||||||
|
<Text as="p" type="secondary" size="small">
|
||||||
|
{t("Imported from {{ source }}", {
|
||||||
|
source:
|
||||||
|
document.sourceName ??
|
||||||
|
`“${document.sourceMetadata.fileName}”`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Heading>{t("Stats")}</Heading>
|
<Heading>{t("Stats")}</Heading>
|
||||||
<Text as="p" type="secondary" size="small">
|
<Text as="p" type="secondary" size="small">
|
||||||
<List>
|
<List>
|
||||||
@@ -108,6 +123,26 @@ function Insights() {
|
|||||||
<Time dateTime={document.updatedAt} addSuffix />.
|
<Time dateTime={document.updatedAt} addSuffix />.
|
||||||
</Text>
|
</Text>
|
||||||
<ListSpacing>
|
<ListSpacing>
|
||||||
|
{document.sourceMetadata?.createdByName && (
|
||||||
|
<ListItem
|
||||||
|
title={document.sourceMetadata?.createdByName}
|
||||||
|
image={
|
||||||
|
<Avatar
|
||||||
|
model={{
|
||||||
|
color: stringToColor(
|
||||||
|
document.sourceMetadata.createdByName
|
||||||
|
),
|
||||||
|
avatarUrl: null,
|
||||||
|
initial: document.sourceMetadata.createdByName[0],
|
||||||
|
}}
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
subtitle={t("Creator")}
|
||||||
|
border={false}
|
||||||
|
small
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<PaginatedList
|
<PaginatedList
|
||||||
aria-label={t("Contributors")}
|
aria-label={t("Contributors")}
|
||||||
items={document.collaborators}
|
items={document.collaborators}
|
||||||
@@ -118,7 +153,9 @@ function Insights() {
|
|||||||
image={<Avatar model={model} size={32} />}
|
image={<Avatar model={model} size={32} />}
|
||||||
subtitle={
|
subtitle={
|
||||||
model.id === document.createdBy?.id
|
model.id === document.createdBy?.id
|
||||||
? t("Creator")
|
? document.sourceMetadata?.createdByName
|
||||||
|
? t("Imported")
|
||||||
|
: t("Creator")
|
||||||
: model.id === document.updatedBy?.id
|
: model.id === document.updatedBy?.id
|
||||||
? t("Last edited")
|
? t("Last edited")
|
||||||
: t("Previously edited")
|
: t("Previously edited")
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ async function presentDocument(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!options.isPublic) {
|
if (!options.isPublic) {
|
||||||
|
const source = await document.$get("import");
|
||||||
|
|
||||||
data.collectionId = document.collectionId;
|
data.collectionId = document.collectionId;
|
||||||
data.parentDocumentId = document.parentDocumentId;
|
data.parentDocumentId = document.parentDocumentId;
|
||||||
data.createdBy = presentUser(document.createdBy);
|
data.createdBy = presentUser(document.createdBy);
|
||||||
@@ -57,6 +59,14 @@ async function presentDocument(
|
|||||||
data.templateId = document.templateId;
|
data.templateId = document.templateId;
|
||||||
data.template = document.template;
|
data.template = document.template;
|
||||||
data.insightsEnabled = document.insightsEnabled;
|
data.insightsEnabled = document.insightsEnabled;
|
||||||
|
data.sourceMetadata = document.sourceMetadata
|
||||||
|
? {
|
||||||
|
importedAt: source?.createdAt ?? document.createdAt,
|
||||||
|
importType: source?.format,
|
||||||
|
createdByName: document.sourceMetadata.createdByName,
|
||||||
|
fileName: document.sourceMetadata?.fileName,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export default class ExportJSONTask extends ExportTask {
|
|||||||
title: document.title,
|
title: document.title,
|
||||||
data: DocumentHelper.toProsemirror(document),
|
data: DocumentHelper.toProsemirror(document),
|
||||||
createdById: document.createdById,
|
createdById: document.createdById,
|
||||||
|
createdByName: document.createdBy.name,
|
||||||
createdByEmail: document.createdBy.email,
|
createdByEmail: document.createdBy.email,
|
||||||
createdAt: document.createdAt.toISOString(),
|
createdAt: document.createdAt.toISOString(),
|
||||||
updatedAt: document.updatedAt.toISOString(),
|
updatedAt: document.updatedAt.toISOString(),
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export type StructuredImportData = {
|
|||||||
publishedAt?: Date | null;
|
publishedAt?: Date | null;
|
||||||
parentDocumentId?: string | null;
|
parentDocumentId?: string | null;
|
||||||
createdById?: string;
|
createdById?: string;
|
||||||
|
createdByName?: string;
|
||||||
createdByEmail?: string | null;
|
createdByEmail?: string | null;
|
||||||
path: string;
|
path: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
@@ -467,6 +468,7 @@ export default abstract class ImportTask extends BaseTask<Props> {
|
|||||||
fileName: path.basename(item.path),
|
fileName: path.basename(item.path),
|
||||||
mimeType: item.mimeType,
|
mimeType: item.mimeType,
|
||||||
externalId: item.externalId,
|
externalId: item.externalId,
|
||||||
|
createdByName: item.createdByName,
|
||||||
},
|
},
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ export type DocumentJSONExport = {
|
|||||||
title: string;
|
title: string;
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
createdById: string;
|
createdById: string;
|
||||||
|
createdByName: string;
|
||||||
createdByEmail: string | null;
|
createdByEmail: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -166,8 +166,9 @@
|
|||||||
"{{ userName }} deleted": "{{ userName }} deleted",
|
"{{ userName }} deleted": "{{ userName }} deleted",
|
||||||
"You archived": "You archived",
|
"You archived": "You archived",
|
||||||
"{{ userName }} archived": "{{ userName }} archived",
|
"{{ userName }} archived": "{{ userName }} archived",
|
||||||
"You created": "You created",
|
|
||||||
"{{ userName }} created": "{{ userName }} created",
|
"{{ userName }} created": "{{ userName }} created",
|
||||||
|
"Imported": "Imported",
|
||||||
|
"You created": "You created",
|
||||||
"You published": "You published",
|
"You published": "You published",
|
||||||
"{{ userName }} published": "{{ userName }} published",
|
"{{ userName }} published": "{{ userName }} published",
|
||||||
"You saved": "You saved",
|
"You saved": "You saved",
|
||||||
@@ -561,6 +562,8 @@
|
|||||||
"Done editing": "Done editing",
|
"Done editing": "Done editing",
|
||||||
"Restore version": "Restore version",
|
"Restore version": "Restore version",
|
||||||
"No history yet": "No history yet",
|
"No history yet": "No history yet",
|
||||||
|
"Source": "Source",
|
||||||
|
"Imported from {{ source }}": "Imported from {{ source }}",
|
||||||
"Stats": "Stats",
|
"Stats": "Stats",
|
||||||
"{{ count }} minute read": "{{ count }} minute read",
|
"{{ count }} minute read": "{{ count }} minute read",
|
||||||
"{{ count }} minute read_plural": "{{ count }} minute read",
|
"{{ count }} minute read_plural": "{{ count }} minute read",
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export type SourceMetadata = {
|
|||||||
fileName?: string;
|
fileName?: string;
|
||||||
/** The original source mime type. */
|
/** The original source mime type. */
|
||||||
mimeType?: string;
|
mimeType?: string;
|
||||||
|
/** The creator of the original external source. */
|
||||||
|
createdByName?: string;
|
||||||
/** An ID in the external source. */
|
/** An ID in the external source. */
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
/** Whether the item was created through a trial license. */
|
/** Whether the item was created through a trial license. */
|
||||||
|
|||||||
Reference in New Issue
Block a user