Skip to content

Commit

Permalink
feat(cli): support link on multiple diagrams
Browse files Browse the repository at this point in the history
Add support for running `mermaid-chart link diagram1.mmd diagram2.mmd`.

The CLI tool will ask the user if they want to upload all diagrams to
the same project. Otherwise it will ask the user for the correct project
for each diagram:

```console
$ npx @mermaidchart/cli link test/output/unsynced.mmd test/output/unsynced1.mmd
? Select a project to upload test/output/unsynced.mmd to personal
? Would you like to upload all 2 diagrams to this project? (Y/n)
```
  • Loading branch information
aloisklink committed Nov 29, 2023
1 parent dbf3c50 commit ae344b7
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 47 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dependencies": {
"@commander-js/extra-typings": "^11.1.0",
"@iarna/toml": "^2.2.5",
"@inquirer/confirm": "^2.0.15",
"@inquirer/input": "^1.2.14",
"@inquirer/select": "^1.3.1",
"@mermaidchart/sdk": "workspace:^",
Expand Down
57 changes: 56 additions & 1 deletion packages/cli/src/commander.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { copyFile, mkdir, readFile, rm } from 'node:fs/promises';
import type { Command, CommanderError, OutputConfiguration } from '@commander-js/extra-typings';
import { MermaidChart } from '@mermaidchart/sdk';

import confirm from '@inquirer/confirm';
import input from '@inquirer/input';
import select from '@inquirer/select';
import type { MCDocument, MCProject, MCUser } from '@mermaidchart/sdk/dist/types.js';
Expand Down Expand Up @@ -186,9 +187,15 @@ describe('logout', () => {

describe('link', () => {
const diagram = 'test/output/unsynced.mmd';
const diagram2 = 'test/output/unsynced2.mmd';
const diagram3 = 'test/output/unsynced3.mmd';

beforeEach(async () => {
await copyFile('test/fixtures/unsynced.mmd', diagram);
await Promise.all([
copyFile('test/fixtures/unsynced.mmd', diagram),
copyFile('test/fixtures/unsynced.mmd', diagram2),
copyFile('test/fixtures/unsynced.mmd', diagram3),
]);
});

it('should create a new diagram on MermaidChart and add id to frontmatter', async () => {
Expand All @@ -214,6 +221,54 @@ describe('link', () => {
`id: ${mockedEmptyDiagram.documentID}`,
);
});

for (const rememberProjectId of [true, false]) {
it(`should link multiple diagrams ${
rememberProjectId ? 'and remember project id' : ''
}`, async () => {
const { program } = mockedProgram();

vi.mock('@inquirer/confirm');
vi.mock('@inquirer/select');
vi.mocked(confirm).mockResolvedValue(rememberProjectId);
vi.mocked(select).mockResolvedValue(mockedProjects[0].id);

vi.mocked(MermaidChart.prototype.createDocument).mockResolvedValue(mockedEmptyDiagram);

await expect(readFile(diagram, { encoding: 'utf8' })).resolves.not.toContain(/^id:/);

await program.parseAsync(['--config', CONFIG_AUTHED, 'link', diagram, diagram2, diagram3], {
from: 'user',
});

if (rememberProjectId) {
expect(vi.mocked(confirm)).toHaveBeenCalledOnce();
expect(vi.mocked(select)).toHaveBeenCalledOnce();
} else {
// if the user didn't allow using the same project id for all diagrams,
// ask every time
expect(vi.mocked(confirm)).toHaveBeenCalledOnce();
expect(vi.mocked(select)).toHaveBeenCalledTimes(3);
}

// should have uploaded and created three files
expect(vi.mocked(MermaidChart.prototype.setDocument)).toHaveBeenCalledTimes(3);
expect(vi.mocked(MermaidChart.prototype.setDocument)).toHaveBeenCalledWith(
expect.objectContaining({
code: expect.not.stringContaining('id:'), // id: field should not be uploaded
title: diagram, // title should default to file name
}),
);

await Promise.all(
[diagram, diagram2, diagram3].map(async (file) => {
await expect(readFile(file, { encoding: 'utf8' })).resolves.toContain(
`id: ${mockedEmptyDiagram.documentID}`,
);
}),
);
});
}
});

describe('pull', () => {
Expand Down
96 changes: 58 additions & 38 deletions packages/cli/src/commander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { readFile, writeFile } from 'fs/promises';
import { extractFrontMatter, removeFrontMatterKeys, injectFrontMatter } from './frontmatter.js';
import { MermaidChart } from '@mermaidchart/sdk';

import confirm from '@inquirer/confirm';
import input from '@inquirer/input';
import select, { Separator } from '@inquirer/select';
import { type Config, defaultConfigPath, readConfig, writeConfig } from './config.js';
Expand Down Expand Up @@ -158,52 +159,71 @@ function logout() {

function link() {
return createCommand('link')
.description('Link the given Mermaid diagram to Mermaid Chart')
.addArgument(new Argument('<path>', 'The path of the file to link.'))
.action(async (path, _options, command) => {
.description('Link the given Mermaid diagrams to Mermaid Chart')
.addArgument(new Argument('<path...>', 'The paths of the files to link.'))
.action(async (paths, _options, command) => {
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
const client = await createClient(optsWithGlobals);
const existingFile = await readFile(path, { encoding: 'utf8' });
const frontmatter = extractFrontMatter(existingFile);

if (frontmatter.metadata.id) {
throw new CommanderError(
/*exitCode=*/ 1,
'EALREADY_LINKED',
'This document already has an `id` field',
);
}

const projects = await client.getProjects();
/** If set, use this projectId for all diagrams you'll link */
let projectId: string | undefined = undefined;

for (const path of paths) {
const existingFile = await readFile(path, { encoding: 'utf8' });
const frontmatter = extractFrontMatter(existingFile);

if (frontmatter.metadata.id) {
throw new CommanderError(
/*exitCode=*/ 1,
'EALREADY_LINKED',
'This document already has an `id` field',
);
}

const projectId = await select({
message: 'Select a project to upload your document to',
choices: [
...projects.map((project) => {
return {
name: project.title,
value: project.id,
};
}),
new Separator(
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
),
],
});
let thisDiagramProjectId: string | undefined = projectId;

if (!thisDiagramProjectId) {
thisDiagramProjectId = await select({
message: `Select a project to upload ${path} to`,
choices: [
...projects.map((project) => {
return {
name: project.title,
value: project.id,
};
}),
new Separator(
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
),
],
});

if (path === paths[0] && paths.length > 1) {
const useProjectIdForAllDiagrams = await confirm({
message: `Would you like to upload all ${paths.length} diagrams to this project?`,
default: true,
});
if (useProjectIdForAllDiagrams) {
projectId = thisDiagramProjectId;
}
}
}

const createdDocument = await client.createDocument(projectId);
const createdDocument = await client.createDocument(thisDiagramProjectId);

const code = injectFrontMatter(existingFile, { id: createdDocument.documentID });
const code = injectFrontMatter(existingFile, { id: createdDocument.documentID });

await Promise.all([
writeFile(path, code, { encoding: 'utf8' }),
client.setDocument({
id: createdDocument.id,
documentID: createdDocument.documentID,
title: path,
code: existingFile,
}),
]);
await Promise.all([
writeFile(path, code, { encoding: 'utf8' }),
client.setDocument({
id: createdDocument.id,
documentID: createdDocument.documentID,
title: path,
code: existingFile,
}),
]);
}
});
}

Expand Down
26 changes: 18 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ae344b7

Please sign in to comment.