From 207982b6288f3c3f616d66d95f6577b9856083d4 Mon Sep 17 00:00:00 2001 From: Han Wang <44352119+hanywang2@users.noreply.github.com> Date: Tue, 5 Jul 2022 16:00:10 -0400 Subject: [PATCH] Display connections list (#120) * Add community link and setup for treeview * Add icon for tree view * Add icons to groups * List docs in activity bar * Add prefill doc * Add UI for selected doc * Create docs on selected doc or input * Revert ISDev to false * Remove settings.json * Add setup to refresh documents * Add new view for connections * Add display of connections * Add connections list display * Update readme and sidebar title * Update connections on auth and logout * Add Discord to README --- server/src/routes/links.ts | 9 +- vscode/README.md | 4 +- vscode/assets/icons/connect-dark.svg | 4 + vscode/assets/icons/connect.svg | 4 + vscode/assets/icons/github.svg | 4 +- vscode/package.json | 16 ++- vscode/src/components/viewProvider.ts | 4 +- vscode/src/extension.ts | 31 +++++- vscode/src/treeviews/connections.ts | 126 ++++++---------------- vscode/src/treeviews/documents.ts | 148 ++++++++++++++++++++++++++ vscode/src/utils/git.ts | 104 +++++++++--------- 11 files changed, 291 insertions(+), 163 deletions(-) create mode 100644 vscode/assets/icons/connect-dark.svg create mode 100644 vscode/assets/icons/connect.svg create mode 100644 vscode/src/treeviews/documents.ts diff --git a/server/src/routes/links.ts b/server/src/routes/links.ts index 85b85f0..937955c 100644 --- a/server/src/routes/links.ts +++ b/server/src/routes/links.ts @@ -12,10 +12,17 @@ linksRouter.get('/', userMiddleware, async (req, res) => { const { org } = res.locals.user; let matchQuery: any = { org: org._id }; - if (req.query?.repo && req.query?.gitOrg) { + if (req.query?.repo) { matchQuery.repo = req.query.repo; + } + + if (req.query?.gitOrg) { matchQuery.gitOrg = req.query.gitOrg; } + + if (req.query?.file) { + matchQuery.file = req.query.file; + } try { const codes = await Code.aggregate([ diff --git a/vscode/README.md b/vscode/README.md index 6de8055..2957fd1 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -1,6 +1,6 @@ -# 🌿 Document Connect +# 🌿 Document Connector -![](https://img.shields.io/github/checks-status/mintlify/mintlify/38f1d5b1fd9397e56f5da3ec2d254b09859a579f) [![Stars](https://img.shields.io/github/stars/mintlify/mintlify?style=social)](https://github.com/mintlify/mintlify) [![Twitter](https://img.shields.io/twitter/follow/mintlify?style=social)](https://twitter.com/mintlify) +![](https://img.shields.io/github/checks-status/mintlify/mintlify/38f1d5b1fd9397e56f5da3ec2d254b09859a579f) [![discord](https://img.shields.io/discord/911693009253466123?logo=Discord&logoColor=white)](https://discord.gg/6W7GuYuxra) [![Stars](https://img.shields.io/github/stars/mintlify/mintlify?style=social)](https://github.com/mintlify/mintlify) [![Twitter](https://img.shields.io/twitter/follow/mintlify?style=social)](https://twitter.com/mintlify) Link documentation to your codebase diff --git a/vscode/assets/icons/connect-dark.svg b/vscode/assets/icons/connect-dark.svg new file mode 100644 index 0000000..56d7668 --- /dev/null +++ b/vscode/assets/icons/connect-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/vscode/assets/icons/connect.svg b/vscode/assets/icons/connect.svg new file mode 100644 index 0000000..c8e1c83 --- /dev/null +++ b/vscode/assets/icons/connect.svg @@ -0,0 +1,4 @@ + + + + diff --git a/vscode/assets/icons/github.svg b/vscode/assets/icons/github.svg index a8d1174..d6386b7 100644 --- a/vscode/assets/icons/github.svg +++ b/vscode/assets/icons/github.svg @@ -1,3 +1,3 @@ - - + + diff --git a/vscode/package.json b/vscode/package.json index 4aeb92b..1f29650 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -47,8 +47,12 @@ "title": "Prefill document" }, { - "command": "mintlify.refresh-docs", - "title": "Refresh documents view" + "command": "mintlify.hightlight-connection", + "title": "Highlight connection" + }, + { + "command": "mintlify.refresh-views", + "title": "Refresh views" }, { "command": "mintlify.open-doc", @@ -77,8 +81,12 @@ "name": "Create connection" }, { - "id": "connections", + "id": "documents", "name": "Documents" + }, + { + "id": "connections", + "name": "Connections" } ] }, @@ -86,7 +94,7 @@ "activitybar": [ { "id": "primary", - "title": "Document Connect", + "title": "Documents", "icon": "assets/icon.png" } ] diff --git a/vscode/src/components/viewProvider.ts b/vscode/src/components/viewProvider.ts index 45e1ff4..9c2bf70 100644 --- a/vscode/src/components/viewProvider.ts +++ b/vscode/src/components/viewProvider.ts @@ -40,7 +40,7 @@ export class ViewProvider implements WebviewViewProvider { this.globalState.setUserId(user.userId); this._view?.webview.postMessage({ command: 'auth', args: user }); vscode.commands.executeCommand('mintlify.refresh-links'); - vscode.commands.executeCommand('mintlify.refresh-docs'); + vscode.commands.executeCommand('mintlify.refresh-views'); } public prefillDocWithDocId = (docId: string) => { @@ -56,7 +56,7 @@ export class ViewProvider implements WebviewViewProvider { public logout(): void { this._view?.webview.postMessage({ command: 'logout' }); this.globalState.clearState(); - vscode.commands.executeCommand('mintlify.refresh-docs'); + vscode.commands.executeCommand('mintlify.refresh-views'); } public resolveWebviewView(webviewView: WebviewView): void | Thenable { diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 3db422a..bfbaffb 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -1,16 +1,27 @@ import * as vscode from 'vscode'; -import { ViewProvider } from './components/viewProvider'; +import { Doc, ViewProvider } from './components/viewProvider'; import { linkCodeCommand, linkDirCommand, refreshLinksCommand, openDocsCommand } from './components/commands'; import { registerAuthRoute } from './components/authentication'; import FileCodeLensProvider from './components/codeLensProvider'; import GlobalState from './utils/globalState'; -import { ConnectionsTreeProvider } from './treeviews/connections'; +import { DocumentsTreeProvider } from './treeviews/documents'; +import { CodeReturned, ConnectionsTreeProvider } from './treeviews/connections'; const createTreeViews = (state: GlobalState): void => { + const documentsTreeProvider = new DocumentsTreeProvider(state); const connectionsTreeProvider = new ConnectionsTreeProvider(state); + vscode.window.createTreeView('documents', { treeDataProvider: documentsTreeProvider }); vscode.window.createTreeView('connections', { treeDataProvider: connectionsTreeProvider }); - vscode.commands.registerCommand('mintlify.refresh-docs', () => { + vscode.commands.registerCommand('mintlify.refresh-views', () => { + documentsTreeProvider.refresh(); + connectionsTreeProvider.refresh(); + }); + + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor == null) { + return; + } connectionsTreeProvider.refresh(); }); }; @@ -36,10 +47,22 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.executeCommand('mintlify.link-code', { editor, scheme: 'file' }); }); - vscode.commands.registerCommand('mintlify.prefill-doc', (doc) => { + vscode.commands.registerCommand('mintlify.prefill-doc', (doc: Doc) => { viewProvider.prefillDoc(doc); }); + vscode.commands.registerCommand('mintlify.highlight-connection', async (code: CodeReturned) => { + if (code.line != null && code.endLine != null) { + const rootPath = vscode.workspace.workspaceFolders![0].uri.path; + const filePathUri = vscode.Uri.parse(`${rootPath}/${code.file}`); + const selectedRange = new vscode.Range(code.line, 0, code.endLine, 9999); + vscode.window.activeTextEditor?.revealRange(selectedRange); + await vscode.window.showTextDocument(filePathUri, { + selection: selectedRange, + preserveFocus: true, + }); + } + }); createTreeViews(globalState); vscode.commands.executeCommand('mintlify.refresh-links', context); } diff --git a/vscode/src/treeviews/connections.ts b/vscode/src/treeviews/connections.ts index f36ca8f..2db5ffe 100644 --- a/vscode/src/treeviews/connections.ts +++ b/vscode/src/treeviews/connections.ts @@ -3,31 +3,25 @@ import * as path from 'path'; import GlobalState from '../utils/globalState'; import axios from 'axios'; import { API_ENDPOINT } from '../utils/api'; +import { Code, getRepoInfo } from '../utils/git'; import { Doc } from '../components/viewProvider'; -type Group = { - _id: string; - name: string; - count: number; - lastUpdatedDoc: Doc; - tasksCount: number; - isLoading: boolean; -}; +export type CodeReturned = Code & { doc: Doc }; -export class ConnectionsTreeProvider implements vscode.TreeDataProvider { +export class ConnectionsTreeProvider implements vscode.TreeDataProvider { private state: GlobalState; - private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; constructor(state: GlobalState) { this.state = state; } - getTreeItem(element: GroupOption): vscode.TreeItem { + getTreeItem(element: ConnectionIcon): vscode.TreeItem { return element; } - async getChildren(groupElement: GroupOption): Promise { + async getChildren(): Promise { const userId = this.state.getUserId(); if (!userId) { return []; @@ -35,36 +29,29 @@ export class ConnectionsTreeProvider implements vscode.TreeDataProvider new DocOption(doc, vscode.TreeItemCollapsibleState.None)); + const editor = vscode.window.activeTextEditor; + let gitOrg, file, repo; + if (editor) { + const fileFsPath: string = editor.document.uri.fsPath; + const { gitOrg: activeGitOrg, repo: activeRepo, file: activeFile } = await getRepoInfo(fileFsPath); + [gitOrg, file, repo] = [activeGitOrg, activeFile, activeRepo]; } - const { data: { groups } } = await axios.get(`${API_ENDPOINT}/docs/groups`, { + const { data: { codes } } = await axios.get(`${API_ENDPOINT}/links`, { params: { userId, - subdomain + subdomain, + gitOrg, + file, + repo } }); - // Add docs to home level when just 1 group - if (groups.length === 1) { - const group = groups[0]; - const { data: { docs } } = await axios.get(`${API_ENDPOINT}/docs/method/${group._id}`, { - params: { - userId, - subdomain - } - }); - return docs.map((doc) => new DocOption(doc, vscode.TreeItemCollapsibleState.None)); + if (codes.length === 0) { + return [new EmptyListIcon()]; } - return [...groups.map((group) => new GroupOption(group, vscode.TreeItemCollapsibleState.Collapsed))]; + return [...codes.map((code) => new ConnectionIcon(code))]; } refresh(): void { @@ -72,77 +59,30 @@ export class ConnectionsTreeProvider implements vscode.TreeDataProvider { - switch (id) { - case 'github': - return { - light: path.join(__filename, '..', '..', 'assets', 'icons', 'github.svg'), - dark: path.join(__filename, '..', '..', 'assets', 'icons', 'github-dark.svg') - }; - case 'notion-private': - return { - light: path.join(__filename, '..', '..', 'assets', 'icons', 'notion.svg'), - dark: path.join(__filename, '..', '..', 'assets', 'icons', 'notion-dark.svg'), - }; - case 'googledocs-private': - return { - light: path.join(__filename, '..', '..', 'assets', 'icons', 'google-docs.svg'), - dark: path.join(__filename, '..', '..', 'assets', 'icons', 'google-docs.svg'), - }; - case 'confluence-private': - return { - light: path.join(__filename, '..', '..', 'assets', 'icons', 'confluence.svg'), - dark: path.join(__filename, '..', '..', 'assets', 'icons', 'confluence.svg'), - }; - case 'web': - return { - light: path.join(__filename, '..', '..', 'assets', 'icons', 'web.svg'), - dark: path.join(__filename, '..', '..', 'assets', 'icons', 'web-dark.svg'), - }; - default: - return ''; - } -}; \ No newline at end of file diff --git a/vscode/src/treeviews/documents.ts b/vscode/src/treeviews/documents.ts new file mode 100644 index 0000000..ef25e71 --- /dev/null +++ b/vscode/src/treeviews/documents.ts @@ -0,0 +1,148 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import GlobalState from '../utils/globalState'; +import axios from 'axios'; +import { API_ENDPOINT } from '../utils/api'; +import { Doc } from '../components/viewProvider'; + +type Group = { + _id: string; + name: string; + count: number; + lastUpdatedDoc: Doc; + tasksCount: number; + isLoading: boolean; +}; + +export class DocumentsTreeProvider implements vscode.TreeDataProvider { + private state: GlobalState; + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(state: GlobalState) { + this.state = state; + } + + getTreeItem(element: GroupOption): vscode.TreeItem { + return element; + } + + async getChildren(groupElement: GroupOption): Promise { + const userId = this.state.getUserId(); + if (!userId) { + return []; + } + + const subdomain = this.state.getSubdomain(); + + if (groupElement) { + const { data: { docs } } = await axios.get(`${API_ENDPOINT}/docs/method/${groupElement.group._id}`, { + params: { + userId, + subdomain + } + }); + return docs.map((doc) => new DocOption(doc, vscode.TreeItemCollapsibleState.None)); + } + + const { data: { groups } } = await axios.get(`${API_ENDPOINT}/docs/groups`, { + params: { + userId, + subdomain + } + }); + + // Add docs to home level when just 1 group + if (groups.length === 1) { + const group = groups[0]; + const { data: { docs } } = await axios.get(`${API_ENDPOINT}/docs/method/${group._id}`, { + params: { + userId, + subdomain + } + }); + return docs.map((doc) => new DocOption(doc, vscode.TreeItemCollapsibleState.None)); + } + + return [...groups.map((group) => new GroupOption(group, vscode.TreeItemCollapsibleState.Collapsed))]; + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } +} + +class GroupOption extends vscode.TreeItem { + constructor( + public readonly group: Group, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + ) { + super(group.name, collapsibleState); + this.tooltip = this.group.name; + this.description = `${this.group.count} documents`; + this.iconPath = getIconPathForGroup(group._id); + } +} + +class DocOption extends vscode.TreeItem { + constructor( + public readonly doc: Doc, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + ) { + super(doc.title, collapsibleState); + this.tooltip = this.doc.title; + this.iconPath = { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'document.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'document-dark.svg'), + }; + + const onClickCommand: vscode.Command = { + title: 'Prefill Doc', + command: 'mintlify.prefill-doc', + arguments: [this.doc] + }; + + this.command = onClickCommand; + } +} + +// TBD: Add doc option +class AddDocOption extends vscode.TreeItem { + constructor() { + super('', vscode.TreeItemCollapsibleState.Collapsed); + this.tooltip = ''; + this.description = 'Add new document'; + } +} + +const getIconPathForGroup = (id: string): string | { light: string, dark: string } => { + switch (id) { + case 'github': + return { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'github.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'github-dark.svg') + }; + case 'notion-private': + return { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'notion.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'notion-dark.svg'), + }; + case 'googledocs-private': + return { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'google-docs.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'google-docs.svg'), + }; + case 'confluence-private': + return { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'confluence.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'confluence.svg'), + }; + case 'web': + return { + light: path.join(__filename, '..', '..', 'assets', 'icons', 'web.svg'), + dark: path.join(__filename, '..', '..', 'assets', 'icons', 'web-dark.svg'), + }; + default: + return ''; + } +}; \ No newline at end of file diff --git a/vscode/src/utils/git.ts b/vscode/src/utils/git.ts index 69c39fe..592769d 100644 --- a/vscode/src/utils/git.ts +++ b/vscode/src/utils/git.ts @@ -154,10 +154,6 @@ export const getGitData = async (fileFsPath: string, viewProvider: ViewProvider, } if (command !== 'code') { await viewProvider.show(); - function delay(time) { - return new Promise(resolve => setTimeout(resolve, time)); - } - await delay(200); } return viewProvider.postCode(code); }); @@ -177,60 +173,58 @@ export const getFilePath = (fileFsPath: string) => { }; export const getRepoInfo = async (fileFsPath: string): Promise<{ repo: string, gitOrg: string, file: string }> => { - const repoDir: string | null = findParentDir.sync(fileFsPath, '.git'); - - let gitOrg: string, repo: string; - - if (!repoDir) { - return { repo: '', gitOrg: '', file: '' }; // TODO - proper error handling - } - - const gitConfigPath: string = await locateGitConfig(repoDir); - const config: {[key: string]: any;} = await readConfigFile(gitConfigPath); - - const file: string = getFilePath(fileFsPath); - - await gitRev.long(repoDir, async function (_: any, sha: any) { - let rawUri: any, - configuredBranch: any, - provider: BaseProvider | null = null, - remoteName: any; - - await gitRev.branch(repoDir, async function (_: any, branch: any) { - // Check to see if the branch has a configured remote - configuredBranch = config[`remote "${branch}"`]; - if (configuredBranch) { - // Use the current branch's configured remote - remoteName = configuredBranch.remote; - rawUri = config[`remote "${remoteName}"`].url; - } else { - const remotes = Object.keys(config).filter(k => k.startsWith('remote ')); - if (remotes.length > 0) { - rawUri = config[remotes[0]].url; + return new Promise(async (resolve, reject) => { + const repoDir: string | null = findParentDir.sync(fileFsPath, '.git'); + let gitOrg: string, repo: string; + if (!repoDir) { + return resolve({ repo: '', gitOrg: '', file: '' }); // TODO - proper error handling + } + + const gitConfigPath: string = await locateGitConfig(repoDir); + const config: {[key: string]: any} = await readConfigFile(gitConfigPath); + + const file: string = getFilePath(fileFsPath); + + await gitRev.long(repoDir, async function (_: any, sha: any) { + let rawUri: any, + configuredBranch: any, + provider: BaseProvider | null = null, + remoteName: any; + + await gitRev.branch(repoDir, async function (_: any, branch: any) { + // Check to see if the branch has a configured remote + configuredBranch = config[`remote "${branch}"`]; + if (configuredBranch) { + // Use the current branch's configured remote + remoteName = configuredBranch.remote; + rawUri = config[`remote "${remoteName}"`].url; + } else { + const remotes = Object.keys(config).filter(k => k.startsWith('remote ')); + if (remotes.length > 0) { + rawUri = config[remotes[0]].url; + } + } + if (!rawUri) { + vscode.window.showWarningMessage(`No remote found on branch.`); + return reject(); } - } - if (!rawUri) { - vscode.window.showWarningMessage(`No remote found on branch.`); - return; - } - try { - provider = gitProvider(rawUri, sha); - } catch (e: any) { - let errmsg = e.toString(); - window.showWarningMessage(`Unknown Git provider. ${errmsg}`); - return; - } + try { + provider = gitProvider(rawUri, sha); + } catch (e: any) { + let errmsg = e.toString(); + window.showWarningMessage(`Unknown Git provider. ${errmsg}`); + return reject(); + } - let formattedFilePath = path.relative(repoDir, fileFsPath).replace(/\\/g, '/'); - formattedFilePath = formattedFilePath.replace(/\s{1}/g, '%20'); - if (provider != null) { - gitOrg = provider.gitUrl.organization; - repo = provider.gitUrl.name; - return { repo, gitOrg, file }; - } + let formattedFilePath = path.relative(repoDir, fileFsPath).replace(/\\/g, '/'); + formattedFilePath = formattedFilePath.replace(/\s{1}/g, '%20'); + if (provider != null) { + gitOrg = provider.gitUrl.organization; + repo = provider.gitUrl.name; + return resolve({ repo, gitOrg, file }); + } + }); }); }); - - return { repo: '', gitOrg: '', file }; // TODO - proper error handling };