diff --git a/app/components/DocumentMeta.tsx b/app/components/DocumentMeta.tsx index 03bd2a473..f6e9f7fbe 100644 --- a/app/components/DocumentMeta.tsx +++ b/app/components/DocumentMeta.tsx @@ -96,7 +96,16 @@ const DocumentMeta: React.FC = ({ ); } else if (createdAt === updatedAt) { - content = ( + content = document.sourceMetadata ? ( + + {document.sourceMetadata.createdByName + ? t("{{ userName }} created", { + userName: document.sourceMetadata.createdByName, + }) + : t("Imported")}{" "} + + ) : ( {lastUpdatedByCurrentUser ? t("You created") diff --git a/app/models/Document.ts b/app/models/Document.ts index d71e88039..deee70d25 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -1,8 +1,13 @@ import { addDays, differenceInDays } from "date-fns"; import i18n, { t } from "i18next"; +import capitalize from "lodash/capitalize"; import floor from "lodash/floor"; 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 Storage from "@shared/utils/Storage"; import { isRTL } from "@shared/utils/rtl"; @@ -56,6 +61,43 @@ export default class Document extends ParanoidModel { @observable 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. */ diff --git a/app/scenes/Document/components/Insights.tsx b/app/scenes/Document/components/Insights.tsx index 47a4dee5c..50bba4adc 100644 --- a/app/scenes/Document/components/Insights.tsx +++ b/app/scenes/Document/components/Insights.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { useHistory, useRouteMatch } from "react-router-dom"; import styled from "styled-components"; import { s } from "@shared/styles"; +import { stringToColor } from "@shared/utils/color"; import User from "~/models/User"; import Avatar from "~/components/Avatar"; import { useDocumentContext } from "~/components/DocumentContext"; @@ -56,6 +57,20 @@ function Insights() { >
+ {document.sourceMetadata && ( + <> + {t("Source")} + { + + {t("Imported from {{ source }}", { + source: + document.sourceName ?? + `“${document.sourceMetadata.fileName}”`, + })} + + } + + )} {t("Stats")} @@ -108,6 +123,26 @@ function Insights() { + {document.sourceMetadata?.createdByName && ( + + } + subtitle={t("Creator")} + border={false} + small + /> + )} } subtitle={ model.id === document.createdBy?.id - ? t("Creator") + ? document.sourceMetadata?.createdByName + ? t("Imported") + : t("Creator") : model.id === document.updatedBy?.id ? t("Last edited") : t("Previously edited") diff --git a/server/presenters/document.ts b/server/presenters/document.ts index 6f7af45d8..8547071df 100644 --- a/server/presenters/document.ts +++ b/server/presenters/document.ts @@ -49,6 +49,8 @@ async function presentDocument( } if (!options.isPublic) { + const source = await document.$get("import"); + data.collectionId = document.collectionId; data.parentDocumentId = document.parentDocumentId; data.createdBy = presentUser(document.createdBy); @@ -57,6 +59,14 @@ async function presentDocument( data.templateId = document.templateId; data.template = document.template; 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; diff --git a/server/queues/tasks/ExportJSONTask.ts b/server/queues/tasks/ExportJSONTask.ts index 3f6f36bb2..bcd93cc5c 100644 --- a/server/queues/tasks/ExportJSONTask.ts +++ b/server/queues/tasks/ExportJSONTask.ts @@ -125,6 +125,7 @@ export default class ExportJSONTask extends ExportTask { title: document.title, data: DocumentHelper.toProsemirror(document), createdById: document.createdById, + createdByName: document.createdBy.name, createdByEmail: document.createdBy.email, createdAt: document.createdAt.toISOString(), updatedAt: document.updatedAt.toISOString(), diff --git a/server/queues/tasks/ImportTask.ts b/server/queues/tasks/ImportTask.ts index ffe2a3fad..b44a812ca 100644 --- a/server/queues/tasks/ImportTask.ts +++ b/server/queues/tasks/ImportTask.ts @@ -78,6 +78,7 @@ export type StructuredImportData = { publishedAt?: Date | null; parentDocumentId?: string | null; createdById?: string; + createdByName?: string; createdByEmail?: string | null; path: string; mimeType: string; @@ -467,6 +468,7 @@ export default abstract class ImportTask extends BaseTask { fileName: path.basename(item.path), mimeType: item.mimeType, externalId: item.externalId, + createdByName: item.createdByName, }, id: item.id, title: item.title, diff --git a/server/types.ts b/server/types.ts index 5d16d3858..029d290c0 100644 --- a/server/types.ts +++ b/server/types.ts @@ -467,6 +467,7 @@ export type DocumentJSONExport = { title: string; data: Record; createdById: string; + createdByName: string; createdByEmail: string | null; createdAt: string; updatedAt: string; diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 58dae18f3..565767a4f 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -166,8 +166,9 @@ "{{ userName }} deleted": "{{ userName }} deleted", "You archived": "You archived", "{{ userName }} archived": "{{ userName }} archived", - "You created": "You created", "{{ userName }} created": "{{ userName }} created", + "Imported": "Imported", + "You created": "You created", "You published": "You published", "{{ userName }} published": "{{ userName }} published", "You saved": "You saved", @@ -561,6 +562,8 @@ "Done editing": "Done editing", "Restore version": "Restore version", "No history yet": "No history yet", + "Source": "Source", + "Imported from {{ source }}": "Imported from {{ source }}", "Stats": "Stats", "{{ count }} minute read": "{{ count }} minute read", "{{ count }} minute read_plural": "{{ count }} minute read", diff --git a/shared/types.ts b/shared/types.ts index c9efee1ff..d1f9f5ac1 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -148,6 +148,8 @@ export type SourceMetadata = { fileName?: string; /** The original source mime type. */ mimeType?: string; + /** The creator of the original external source. */ + createdByName?: string; /** An ID in the external source. */ externalId?: string; /** Whether the item was created through a trial license. */