Compare commits

...

6 Commits

Author SHA1 Message Date
Saumya Pandey 83361f4dbb fix: remove currentColor 2022-03-16 00:40:21 +05:30
Saumya Pandey b5364bdc60 yarn install 2022-03-16 00:25:40 +05:30
Saumya Pandey 455998074c add tests for collectionImporter command 2022-03-16 00:24:05 +05:30
Saumya Pandey e5e9790c59 fix: use appropriate variable name 2022-03-15 23:29:04 +05:30
Saumya Pandey 6d795e5d73 fix: update relative links with document url 2022-03-15 23:29:04 +05:30
Saumya Pandey 713e27c14a generate relative links on export 2022-03-15 23:29:04 +05:30
7 changed files with 175 additions and 42 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ function Import() {
<div>
<Item
border={false}
image={<OutlineLogo size={28} fill="currentColor" />}
image={<OutlineLogo size={28} />}
title="Outline"
subtitle={t(
"Import a backup file that was previously exported from Outline"
@@ -82,4 +82,67 @@ describe("collectionImporter", () => {
"Uploaded file does not contain importable documents"
);
});
it("should generate valid links in documents when the import zip has relatively linked documents", async () => {
const user = await buildUser();
const name = "Import-Export.zip";
const file = new File({
name,
type: "application/zip",
path: path.resolve(__dirname, "..", "test", "fixtures", name),
});
const response = await collectionImporter({
type: "outline",
user,
file,
ip,
});
expect(response.collections.length).toEqual(1);
expect(response.documents.length).toEqual(8);
expect(response.attachments.length).toEqual(2);
expect(await Collection.count()).toEqual(1);
expect(await Document.count()).toEqual(8);
expect(await Attachment.count()).toEqual(2);
const brainStorm = response.documents.find(
(doc) => doc.title === "Brainstorm"
);
const meetingNotes = response.documents.find(
(doc) => doc.title === "Meeting Notes"
);
const tables = response.documents.find((doc) => doc.title === "Tables");
const github = response.documents.find((doc) => doc.title === "GitHub");
const myfirstDoc = response.documents.find(
(doc) => doc.title === "My first document"
);
const codeTesting = response.documents.find(
(doc) => doc.title === "Code Testing"
);
const testDoc = response.documents.find((doc) => doc.title === "Test Doc");
expect(brainStorm).toBeDefined();
expect(meetingNotes).toBeDefined();
expect(tables).toBeDefined();
expect(github).toBeDefined();
expect(myfirstDoc).toBeDefined();
expect(codeTesting).toBeDefined();
expect(testDoc).toBeDefined();
expect(brainStorm?.text).toContain(
`- [ ] Add notes in [Meeting Notes](/doc/meeting-notes-${meetingNotes?.urlId}) of the last week all hands.`
);
expect(brainStorm?.text).toContain(
`- [ ] See If we can update the table in [Tables](/doc/tables-${tables?.urlId})`
);
expect(github?.text).toContain(
`We should do some [Code Testing](/doc/code-testing-${codeTesting?.urlId}) of the attached GitHub gist.`
);
expect(myfirstDoc?.text).toContain(
`This is a test link to [Test Doc](/doc/test-doc-${testDoc?.urlId})`
);
// My first doc has reference to itself, we don't do anything to it.
expect(myfirstDoc?.text).toContain(
"This is a test link to myself [My first document](/doc/my-first-document-NTU3G1Vbin)"
);
});
});
+28
View File
@@ -188,6 +188,34 @@ async function collectionImporter({
}
}
const updatingRelativeLinksToDocumentUrl = Object.keys(documents).map(
(pathInZip) => {
const document = documents[pathInZip];
const relativeLinks = [...document.text.matchAll(/]\((.\/.*.md)\)/g)].map(
(match) => "." + decodeURI(match[1])
);
relativeLinks.forEach((relativeLink) => {
const linkedDocumentPath = path
.resolve(pathInZip, relativeLink)
.replace(process.cwd() + "/", "");
const linkedDocument = documents[linkedDocumentPath];
if (linkedDocument) {
document.text = document.text.replace(
encodeURI(relativeLink).slice(1),
linkedDocument.url
);
}
});
return document.save({
fields: ["text"],
});
}
);
await Promise.all(updatingRelativeLinksToDocumentUrl);
// reload collections to get document mapping
for (const collection of values(collections)) {
await collection.reload();
Binary file not shown.
+79 -20
View File
@@ -25,7 +25,8 @@ export type Item = {
async function addDocumentTreeToArchive(
zip: JSZip,
documents: NavigationNode[]
documents: NavigationNode[],
documentPathInArchive: Map<string, string>
) {
for (const doc of documents) {
const document = await Document.findByPk(doc.id);
@@ -47,6 +48,22 @@ async function addDocumentTreeToArchive(
text = text.replace(attachment.redirectUrl, encodeURI(attachment.key));
}
const matchedDocumentUrls = [...text.matchAll(/\/doc\/[\w+-]+\w{10}/g)];
matchedDocumentUrls.forEach((match) => {
const matchedUrl = match[0];
const pathofCurrentDoc = documentPathInArchive.get(document.url);
const matchedDocPath = documentPathInArchive.get(matchedUrl);
if (matchedDocPath && pathofCurrentDoc) {
const relativePath = path.relative(pathofCurrentDoc, matchedDocPath);
if (relativePath.startsWith(".")) {
text = text.replace(matchedUrl, encodeURI(relativePath.substring(1)));
}
}
});
let title = serializeFilename(document.title) || "Untitled";
title = safeAddFileToArchive(zip, `${title}.md`, text, {
@@ -61,7 +78,11 @@ async function addDocumentTreeToArchive(
const folder = zip.folder(path.parse(title).name);
if (folder) {
await addDocumentTreeToArchive(folder, doc.children);
await addDocumentTreeToArchive(
folder,
doc.children,
documentPathInArchive
);
}
}
}
@@ -88,6 +109,26 @@ async function addImageToArchive(zip: JSZip, key: string) {
}
}
const getSafeFilename = (files: string[], root: string, key: string) => {
const keysInDirectory = files
.filter((k) => k.includes(root))
.filter((k) => !k.endsWith("/"))
.map((k) => path.basename(k).replace(/\s\((\d+)\)\./, "."));
// The number of duplicate filenames
const existingKeysCount = keysInDirectory.filter((t) => t === key).length;
const filename = path.parse(key).name;
const extension = path.extname(key);
// Construct the new de-duplicated filename (if any)
const safeKey =
existingKeysCount > 0
? `${filename} (${existingKeysCount})${extension}`
: key;
return safeKey;
};
/**
* Adds content to a zip file, if the given filename already exists in the zip
* then it will automatically increment numbers at the end of the filename.
@@ -106,23 +147,7 @@ function safeAddFileToArchive(
) {
// @ts-expect-error root exists
const root = zip.root;
// Filenames in the directory already
const keysInDirectory = Object.keys(zip.files)
.filter((k) => k.includes(root))
.filter((k) => !k.endsWith("/"))
.map((k) => path.basename(k).replace(/\s\((\d+)\)\./, "."));
// The number of duplicate filenames
const existingKeysCount = keysInDirectory.filter((t) => t === key).length;
const filename = path.parse(key).name;
const extension = path.extname(key);
// Construct the new de-duplicated filename (if any)
const safeKey =
existingKeysCount > 0
? `${filename} (${existingKeysCount})${extension}`
: key;
const safeKey = getSafeFilename(Object.keys(zip.files), root, key);
zip.file(safeKey, content, options);
return safeKey;
@@ -158,15 +183,49 @@ async function archiveToPath(zip: JSZip) {
});
}
function getDocumentPathInArchive(
documents: NavigationNode[] | null,
path: string,
mapper: Map<string, string>
) {
if (!documents) {
return;
}
for (const doc of documents) {
const title = serializeFilename(doc.title) || "Untitled";
const filename = getSafeFilename([...mapper.values()], path, `${title}.md`);
mapper.set(doc.url, path + "/" + filename);
if (doc.children && doc.children.length) {
getDocumentPathInArchive(doc.children, path + "/" + title, mapper);
}
}
}
function calculateDocumentPaths(collections: Collection[]) {
const mapper = new Map<string, string>();
for (const collection of collections) {
const basePath = "/" + path.parse(collection.name).name;
getDocumentPathInArchive(collection.documentStructure, basePath, mapper);
}
return mapper;
}
export async function archiveCollections(collections: Collection[]) {
const zip = new JSZip();
const documentPathInArchive = calculateDocumentPaths(collections);
for (const collection of collections) {
if (collection.documentStructure) {
const folder = zip.folder(collection.name);
if (folder) {
await addDocumentTreeToArchive(folder, collection.documentStructure);
await addDocumentTreeToArchive(
folder,
collection.documentStructure,
documentPathInArchive
);
}
}
}
+4 -3
View File
@@ -25,8 +25,8 @@
"Import document": "Import document",
"Templatize": "Templatize",
"Create template": "Create template",
"Home": "Home",
"Search documents for \"{{searchQuery}}\"": "Search documents for \"{{searchQuery}}\"",
"Home": "Home",
"Drafts": "Drafts",
"Templates": "Templates",
"Archive": "Archive",
@@ -135,6 +135,7 @@
"Change Language": "Change Language",
"Dismiss": "Dismiss",
"Back": "Back",
"Documents": "Documents",
"Document archived": "Document archived",
"Move document": "Move document",
"Collections": "Collections",
@@ -290,7 +291,6 @@
"Suspend account": "Suspend account",
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"Documents": "Documents",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
"This collection is only visible to those given access": "This collection is only visible to those given access",
"Private": "Private",
@@ -429,6 +429,7 @@
"Offline": "Offline",
"We were unable to load the document while offline.": "We were unable to load the document while offline.",
"Your account has been suspended": "Your account has been suspended",
"Warning Sign": "Warning Sign",
"A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.": "A team admin (<em>{{ suspendedContactEmail }}</em>) has suspended your account. To re-activate your account, please reach out to them directly.",
"Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.": "Are you sure about that? Deleting the <em>{{groupName}}</em> group will cause its members to lose access to collections and documents that it is associated with.",
"You can edit the name of this group at any time, however doing so too often might confuse your team mates.": "You can edit the name of this group at any time, however doing so too often might confuse your team mates.",
@@ -520,6 +521,7 @@
"Author": "Author",
"We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.",
"No documents found for your search filters.": "No documents found for your search filters.",
"Search Results": "Search Results",
"Processing": "Processing",
"Expired": "Expired",
"Failed": "Failed",
@@ -617,7 +619,6 @@
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"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.",
"Shared documents": "Shared documents",
"Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect Outline to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
-18
View File
@@ -4605,24 +4605,6 @@ boolbase@^1.0.0, boolbase@~1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
boundless-arrow-key-navigation@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/boundless-arrow-key-navigation/-/boundless-arrow-key-navigation-1.1.0.tgz#9b7908a32e2e8f8c1c6af3af68586fdcfe5c40ff"
integrity sha1-m3kIoy4uj4wcavOvaFhv3P5cQP8=
dependencies:
boundless-utils-omit-keys "^1.1.0"
boundless-utils-uuid "^1.1.0"
boundless-utils-omit-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/boundless-utils-omit-keys/-/boundless-utils-omit-keys-1.1.0.tgz#fae73cdb90c113d56201d0b62e8f1143e0d193be"
integrity sha1-+uc825DBE9ViAdC2Lo8RQ+DRk74=
boundless-utils-uuid@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/boundless-utils-uuid/-/boundless-utils-uuid-1.1.0.tgz#ae709f1d4fd3a4557ad4a5c77b1f0a9f701e3ed3"
integrity sha1-rnCfHU/TpFV61KXHex8Kn3AePtM=
bowser@2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9"