Skip to content

Commit 273322b

Browse files
committed
feat(cli): support link on multiple diagrams
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) ```
1 parent 9811ecb commit 273322b

File tree

4 files changed

+117
-37
lines changed

4 files changed

+117
-37
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"dependencies": {
5454
"@commander-js/extra-typings": "^11.1.0",
5555
"@iarna/toml": "^2.2.5",
56+
"@inquirer/confirm": "^2.0.15",
5657
"@inquirer/input": "^1.2.14",
5758
"@inquirer/select": "^1.3.1",
5859
"@mermaidchart/sdk": "workspace:^",

packages/cli/src/commander.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { copyFile, mkdir, readFile, rm } from 'node:fs/promises';
44
import type { Command, CommanderError, OutputConfiguration } from '@commander-js/extra-typings';
55
import { MermaidChart } from '@mermaidchart/sdk';
66

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

187188
describe('link', () => {
188189
const diagram = 'test/output/unsynced.mmd';
190+
const diagram2 = 'test/output/unsynced2.mmd';
191+
const diagram3 = 'test/output/unsynced3.mmd';
189192

190193
beforeEach(async () => {
191-
await copyFile('test/fixtures/unsynced.mmd', diagram);
194+
await Promise.all([
195+
copyFile('test/fixtures/unsynced.mmd', diagram),
196+
copyFile('test/fixtures/unsynced.mmd', diagram2),
197+
copyFile('test/fixtures/unsynced.mmd', diagram3),
198+
]);
192199
});
193200

194201
it('should create a new diagram on MermaidChart and add id to frontmatter', async () => {
@@ -214,6 +221,54 @@ describe('link', () => {
214221
`id: ${mockedEmptyDiagram.documentID}`,
215222
);
216223
});
224+
225+
for (const rememberProjectId of [true, false]) {
226+
it(`should link multiple diagrams ${
227+
rememberProjectId ? 'and remember project id' : ''
228+
}`, async () => {
229+
const { program } = mockedProgram();
230+
231+
vi.mock('@inquirer/confirm');
232+
vi.mock('@inquirer/select');
233+
vi.mocked(confirm).mockResolvedValue(rememberProjectId);
234+
vi.mocked(select).mockResolvedValue(mockedProjects[0].id);
235+
236+
vi.mocked(MermaidChart.prototype.createDocument).mockResolvedValue(mockedEmptyDiagram);
237+
238+
await expect(readFile(diagram, { encoding: 'utf8' })).resolves.not.toContain(/^id:/);
239+
240+
await program.parseAsync(['--config', CONFIG_AUTHED, 'link', diagram, diagram2, diagram3], {
241+
from: 'user',
242+
});
243+
244+
if (rememberProjectId) {
245+
expect(vi.mocked(confirm)).toHaveBeenCalledOnce();
246+
expect(vi.mocked(select)).toHaveBeenCalledOnce();
247+
} else {
248+
// if the user didn't allow using the same project id for all diagrams,
249+
// ask every time
250+
expect(vi.mocked(confirm)).toHaveBeenCalledOnce();
251+
expect(vi.mocked(select)).toHaveBeenCalledTimes(3);
252+
}
253+
254+
// should have uploaded and created three files
255+
expect(vi.mocked(MermaidChart.prototype.setDocument)).toHaveBeenCalledTimes(3);
256+
expect(vi.mocked(MermaidChart.prototype.setDocument)).toHaveBeenCalledWith(
257+
expect.objectContaining({
258+
code: expect.not.stringContaining('id:'), // id: field should not be uploaded
259+
title: diagram, // title should default to file name
260+
}),
261+
);
262+
263+
await Promise.all(
264+
[diagram, diagram2, diagram3].map(async (file) => {
265+
await expect(readFile(file, { encoding: 'utf8' })).resolves.toContain(
266+
`id: ${mockedEmptyDiagram.documentID}`,
267+
);
268+
}),
269+
);
270+
});
271+
}
217272
});
218273

219274
describe('pull', () => {

packages/cli/src/commander.ts

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { readFile, writeFile } from 'fs/promises';
88
import { MermaidChart } from '@mermaidchart/sdk';
99

10+
import confirm from '@inquirer/confirm';
1011
import input from '@inquirer/input';
1112
import select, { Separator } from '@inquirer/select';
1213
import { type Config, defaultConfigPath, readConfig, writeConfig } from './config.js';
@@ -158,39 +159,52 @@ function logout() {
158159

159160
function linkCmd() {
160161
return createCommand('link')
161-
.description('Link the given Mermaid diagram to Mermaid Chart')
162-
.addArgument(new Argument('<path>', 'The path of the file to link.'))
163-
.action(async (path, _options, command) => {
162+
.description('Link the given Mermaid diagrams to Mermaid Chart')
163+
.addArgument(new Argument('<path...>', 'The paths of the files to link.'))
164+
.action(async (paths, _options, command) => {
164165
const optsWithGlobals = command.optsWithGlobals<CommonOptions>();
165166
const client = await createClient(optsWithGlobals);
166167
const linkCache = {};
167168

168-
const existingFile = await readFile(path, { encoding: 'utf8' });
169-
170-
const linkedDiagram = await link(existingFile, client, {
171-
cache: linkCache,
172-
title: path,
173-
async getProjectId(cache) {
174-
cache.projects = cache.projects ?? client.getProjects();
175-
const projectId = await select({
176-
message: `Select a project to upload ${path} to`,
177-
choices: [
178-
...(await cache.projects).map((project) => {
179-
return {
180-
name: project.title,
181-
value: project.id,
182-
};
183-
}),
184-
new Separator(
185-
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
186-
),
187-
],
188-
});
189-
return projectId;
190-
},
191-
});
169+
for (const path of paths) {
170+
const existingFile = await readFile(path, { encoding: 'utf8' });
171+
172+
const linkedDiagram = await link(existingFile, client, {
173+
cache: linkCache,
174+
title: path,
175+
async getProjectId(cache) {
176+
cache.projects = cache.projects ?? client.getProjects();
177+
const projectId = await select({
178+
message: `Select a project to upload ${path} to`,
179+
choices: [
180+
...(await cache.projects).map((project) => {
181+
return {
182+
name: project.title,
183+
value: project.id,
184+
};
185+
}),
186+
new Separator(
187+
`Or go to ${new URL('/app/projects', client.baseURL)} to create a new project`,
188+
),
189+
],
190+
});
191+
192+
if (path === paths[0] && paths.length > 1) {
193+
const useProjectIdForAllDiagrams = await confirm({
194+
message: `Would you like to upload all ${paths.length} diagrams to this project?`,
195+
default: true,
196+
});
197+
if (useProjectIdForAllDiagrams) {
198+
cache.selectedProjectId = projectId;
199+
}
200+
}
201+
202+
return projectId;
203+
},
204+
});
192205

193-
await writeFile(path, linkedDiagram, { encoding: 'utf8' });
206+
await writeFile(path, linkedDiagram, { encoding: 'utf8' });
207+
}
194208
});
195209
}
196210

pnpm-lock.yaml

Lines changed: 18 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)