import * as vscode from "vscode"; import axios from "axios"; import { writeFile, unlink } from "fs"; import { UserDataFolder } from "../common/UserDataFolder"; import { RestyaBoard, RestyaboardList, RestyaboardCard, RestyaboardChecklist, RestyaboardActionComment, CheckItem, RestyaboardMember, RestyaboardLabel } from "./restyaboardComponents"; import { RestyaboardItem } from "./RestyaboardItem"; import { VSCODE_VIEW_COLUMN, TEMP_RESTYABOARD_FILE_NAME, SETTING_PREFIX, SETTING_CONFIG, GLOBALSTATE_CONFIG, } from "./constants"; export class RestyaboardUtils { private globalState: any; private API_TOKEN: string | undefined; private SITE_URL: string | undefined; private tempRestyaboardFile: string; constructor(context?: vscode.ExtensionContext) { this.globalState = context ? context.globalState : {}; this.tempRestyaboardFile = new UserDataFolder().getPathCodeSettings() + TEMP_RESTYABOARD_FILE_NAME || ""; this.getCredentials(); this.setMarkdownPreviewBreaks(); } setMarkdownPreviewBreaks(): void { try { const config = vscode.workspace.getConfiguration("markdown.preview", null); const showPreviewBreaks = config.get<boolean>("breaks"); if (!showPreviewBreaks) { config.update("breaks", true, true); } } catch (error) { console.error(error); } } isCredentialsProvided(): boolean { return (!this.API_TOKEN && !this.SITE_URL); } getCredentials(): void { try { this.API_TOKEN = this.globalState.get(GLOBALSTATE_CONFIG.API_TOKEN); this.SITE_URL = this.globalState.get(GLOBALSTATE_CONFIG.SITE_URL); } catch (error) { console.error(error); vscode.window.showErrorMessage("Error getting credentials"); } } setRestyaboardCredential(isPassword: boolean, placeHolderText: string): Thenable<string | undefined> { return vscode.window.showInputBox({ ignoreFocusOut: true, password: isPassword, placeHolder: placeHolderText }); } // Allows user to set api key and token directly using the vscode input box async setCredentials(): Promise<void> { try { const siteURL = await this.setRestyaboardCredential(false, "Your RestyaBoard URL"); const apiToken = await this.setRestyaboardCredential(true, "Your Restyaboard API token"); if (siteURL !== undefined) this.globalState.update(GLOBALSTATE_CONFIG.SITE_URL, siteURL); if (apiToken !== undefined) this.globalState.update(GLOBALSTATE_CONFIG.API_TOKEN, apiToken); this.getCredentials(); } catch (error) { console.error(error); vscode.window.showErrorMessage("Error while setting credentials"); } } // Generates a Restyaboard API token and opens link in external browser async fetchApiToken(siteURL: string): Promise<void> { const apiTokenUrl = `${siteURL}/oauth/authorize?response_type=code&client_id=9335847554774492&scope=read%20write&state=1562312999016&redirect_uri=${siteURL}/apps/r_visualstudio/login.html`; try { vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(apiTokenUrl)); const apiToken = await this.setRestyaboardCredential(true, "Your Restya Access token"); if (apiToken !== undefined) this.globalState.update(GLOBALSTATE_CONFIG.API_TOKEN, apiToken); if (siteURL !== undefined) this.globalState.update(GLOBALSTATE_CONFIG.SITE_URL, siteURL); } catch (error) { console.error(error); vscode.window.showErrorMessage("Error fetching API token"); } } // Opens browser links for user to get Restyaboard API Key and then Token async authenticate(): Promise<void> { try { const siteURL = await this.setRestyaboardCredential(false, "Your RestyaBoard URL"); if (siteURL !== undefined) { this.globalState.update(GLOBALSTATE_CONFIG.SITE_URL, siteURL); await this.fetchApiToken(siteURL); this.getCredentials(); } else { await vscode.window.showInformationMessage( "Get your Restyaboard Access Token by entering your restyaboard URL"); } vscode.commands.executeCommand("restyaboardViewer.refresh"); } catch (error) { console.error(error); vscode.window.showErrorMessage("Error during authentication"); } } // Deletes all saved info in globalstate (key, token, favouriteList) resetCredentials(): void { Object.keys(GLOBALSTATE_CONFIG).forEach(key => { const value: string = GLOBALSTATE_CONFIG[key]; this.globalState.update(value, undefined); }); vscode.window.showInformationMessage("Credentials have been reset"); this.getCredentials(); vscode.commands.executeCommand("restyaboardViewer.refresh"); } showRestyaboardInfo(): void { this.getCredentials(); const info = ` API_TOKEN = ${this.API_TOKEN}, SITE_URL = ${this.SITE_URL} `; vscode.window.showInformationMessage(info); } async showSuccessMessage(msg: string, url?: string) { let cardUrl; if (url) { cardUrl = await vscode.window.showInformationMessage(msg, url); if (cardUrl) { vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(cardUrl)); } } else { vscode.window.showInformationMessage(msg); } } async restyaboardApiGetRequest(url: string, params: object): Promise<any> { try { axios.defaults.baseURL = this.SITE_URL; const res = await axios.get(url, { params }); return (res.data.data)? res.data.data: []; } catch (error) { if (error.response) { console.error("GET error", error.response); vscode.window.showErrorMessage(`HTTP error: ${error.response.status} - ${error.response.data}`); } } return null; } async restyaboardApiSingleGetRequest(url: string, params: object): Promise<any> { try { axios.defaults.baseURL = this.SITE_URL; const res = await axios.get(url, { params }); return res.data; } catch (error) { if (error.response) { console.error("GET error", error.response); vscode.window.showErrorMessage(`HTTP error: ${error.response.status} - ${error.response.data}`); } } return null; } async restyaboardApiPostRequest(url: string, data: object): Promise<any> { try { axios.defaults.baseURL = this.SITE_URL; const res = await axios.post(url, data); return res; } catch (error) { if (error.response) { console.error("POST error", error.response); vscode.window.showErrorMessage(`HTTP error: ${error.response.status} - ${error.response.data}`); } } return null; } async restyaboardApiPutRequest(url: string, data: object): Promise<any> { try { axios.defaults.baseURL = this.SITE_URL; const res = await axios.put(url, data); return res.data; } catch (error) { if (error.response) { console.error("PUT error", error.response); vscode.window.showErrorMessage(`HTTP error: ${error.response.status} - ${error.response.data}`); } } return null; } async restyaboardApiDeleteRequest(url: string, params: object): Promise<any> { try { axios.defaults.baseURL = this.SITE_URL; const res = await axios.delete(url, { params }); return res.data; } catch (error) { if (error.response) { console.error("DELETE error", error.response); vscode.window.showErrorMessage(`HTTP error: ${error.response.status} - ${error.response.data}`); } } return null; } async getBoardById(boardId: string): Promise<RestyaBoard> { const res = await this.restyaboardApiSingleGetRequest(`/api/v1/boards/${boardId}.json`, { token: this.API_TOKEN, }); return res; } async getListById(listId: string): Promise<RestyaboardList> { const list = await this.restyaboardApiGetRequest(`/api/v1/lists/${listId}`, { token: this.API_TOKEN, }); return list; } getBoards(starredBoards?: boolean): Promise<RestyaBoard[]> { const res = this.restyaboardApiGetRequest("/api/v1/boards.json", { filter: starredBoards ? "starred" : "all", type: 'simple', token: this.API_TOKEN, }); console.log(JSON.stringify(res)); return res; } getListsFromBoard(boardId: string): Promise<RestyaboardList[]> { const res = this.restyaboardApiGetRequest(`/api/v1/boards/${boardId}/lists.json`, { token: this.API_TOKEN, }); return res; } getCardsFromList(listId: string, boardId: string): Promise<RestyaboardCard[]> { const res = this.restyaboardApiGetRequest(`/api/v1/boards/${boardId}/lists/${listId}/cards.json`, { token: this.API_TOKEN, }); return res; } getCardById(cardId: string, boardId: string | undefined, listId: string | undefined): Promise<RestyaboardCard> { const res = this.restyaboardApiSingleGetRequest(`/api/v1/boards/${boardId}/lists/${listId}/cards/${cardId}.json`, { token: this.API_TOKEN, }); return res; } getcardComments(cardId: string, boardId: string | undefined, listId: string | undefined): Promise<RestyaboardCard> { const res = this.restyaboardApiGetRequest(`/api/v1/boards/${boardId}/lists/${listId}/cards/${cardId}/activities.json`, { token: this.API_TOKEN, view:'modal_card', mode:'comment' }); return res; } async addCardToList(list: RestyaboardItem): Promise<Number> { if (!list) { vscode.window.showErrorMessage("Could not get valid list"); return 1; } const cardName = await vscode.window.showInputBox({ ignoreFocusOut: true, placeHolder: "Enter name of card" }); if (cardName === undefined) return 2; const resData = await this.restyaboardApiPostRequest(`/api/v1/boards/${list.boardId}/lists/${list.id}/cards.json?token=${this.API_TOKEN}`, { list_id: list.id, name: cardName, is_offline:true, board_id:list.boardId, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); let card_url = this.SITE_URL+'/#/board/'+list.boardId+'/card/'+resData.data.activity.card_id; this.showSuccessMessage(`Created Card: ${resData.data.activity.card_id}-${resData.data.activity.card_name}`, card_url); return 0; } async editTitle(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid card"); return 1; } const restyaboardCard: RestyaboardCard = await this.getCardById(card.id, card.boardId, card.listId); const name = await vscode.window.showInputBox({ ignoreFocusOut: true, value: restyaboardCard.title }); if (name === undefined) return 2; const resData = await this.restyaboardApiPutRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}.json?token=${this.API_TOKEN}`, { name, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Updated title for card: ${name}`); return 0; } async editDescription(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid card"); return 1; } const restyaboardCard: RestyaboardCard = await this.getCardById(card.id, card.boardId, card.listId); // parse new line chars and remove quotes from start and end let descRaw = JSON.stringify(restyaboardCard.description); descRaw = descRaw.slice(1, descRaw.length - 1); const descUpdated = await vscode.window.showInputBox({ ignoreFocusOut: true, value: descRaw, placeHolder: "Enter description for card", }); if (descUpdated === undefined) return 2; // replaces "\n" with javascript return character required for Restyaboard api const description = descUpdated.replace(/\\n/g, "\x0A"); const resData = await this.restyaboardApiPutRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}.json?token=${this.API_TOKEN}`, { description, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Updated description for card: ${resData.activity.description}`); return 0; } async addComment(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid card"); return 1; } const user: any = await this.getSelf(); const comment = await vscode.window.showInputBox({ ignoreFocusOut: true, placeHolder: "Add comment", }); if (comment === undefined) return 2; const resData = await this.restyaboardApiPostRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}/comments.json?token=${this.API_TOKEN}`, { board_id:card.boardId, card_id:card.id, is_offline:true, list_id:card.listId, user_id:user.id, comment: comment, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Added comment to card: ${card.label}`); return 0; } private getSelf(): Promise<RestyaboardMember> { return this.restyaboardApiSingleGetRequest(`/api/v1/users/me.json`, { token: this.API_TOKEN, }); } async addSelfToCard(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid Card"); return 1; } const user: any = await this.getSelf(); const resData = await this.restyaboardApiPostRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}/users/${user.id}.json?token=${this.API_TOKEN}`, { card_id: card.id, user_id: user.id, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Added user ${user.user.initials} to card`); return 0; } async removeSelfFromCard(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid Card"); return 1; } const user: any = await this.getSelf(); const usersOnCard = await this.getCardById(card.id, card.boardId, card.listId); if (!usersOnCard.cards_users && !usersOnCard.cards_users.length) { vscode.window.showErrorMessage("Card not assigned to you!."); return 3; } let removeUser = usersOnCard.cards_users.find((useroncard: any) => useroncard.user_id == user.id); if (!removeUser){ vscode.window.showErrorMessage("Card not assigned to you!."); return 3; } const resData = await this.restyaboardApiDeleteRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}/cards_users/${removeUser.id}.json`, { token: this.API_TOKEN, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Removed user ${user.user.initials} from card`); return 0; } async addUserToCard(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid Card"); return 1; } const board_details = await this.getBoardById(card.boardId || "-1"); const usersOnBoard = board_details.boards_users; if (!usersOnBoard) return 3; const quickPickUsers = usersOnBoard.map((user: any) => { return { label: user.full_name, userId: user.user_id, }; }); const addUser : any = await vscode.window.showQuickPick(quickPickUsers, { placeHolder: "Add user from board:" }); if (addUser === undefined) return 2; const resData = await this.restyaboardApiPostRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}/users/${addUser.userId}.json?token=${this.API_TOKEN}`, { card_id: card.id, user_id: addUser.userId, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Added user ${addUser.label} to card`); return 0; } async removeUserFromCard(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid Card"); return 1; } const usersOnCard = await this.getCardById(card.id, card.boardId, card.listId); if (!usersOnCard) return 3; const quickPickUsers = usersOnCard.cards_users.map((user: any) => { return { label: user.full_name, userId: user.id, }; }); const removeUser : any = await vscode.window.showQuickPick(quickPickUsers, { placeHolder: "Remove user from board:" }); if (removeUser === undefined) return 2; const resData = await this.restyaboardApiDeleteRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}/cards_users/${removeUser.userId}.json`, { token: this.API_TOKEN, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Removed user ${removeUser.label} from card`); return 0; } async moveCardToList(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid card"); return 1; } const listsForBoard = await this.getListsFromBoard(card.boardId || "-1"); if (!listsForBoard) return 3; const quickPickLists = listsForBoard.map(list => { return { label: list.name, listId: list.id, }; }); const restyaboardCard: RestyaboardCard = await this.getCardById(card.id, card.boardId, card.listId); const toList = await vscode.window.showQuickPick(quickPickLists, { placeHolder: "Move card to list:" }); if (toList === undefined) return 2; const resData = await this.restyaboardApiPutRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}.json?token=${this.API_TOKEN}`, { list_id: toList.listId, board_id:card.boardId, position:restyaboardCard.position }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); this.showSuccessMessage(`Moved card to list: ${toList.label}`); return 0; } async archiveCard(card: RestyaboardItem): Promise<Number> { if (!card) { vscode.window.showErrorMessage("Could not get valid card"); return 1; } const resData = await this.restyaboardApiPutRequest(`/api/v1/boards/${card.boardId}/lists/${card.listId}/cards/${card.id}.json?token=${this.API_TOKEN}`, { is_archived: 1, }); if (!resData) return 3; vscode.commands.executeCommand("restyaboardViewer.refresh"); let card_url = this.SITE_URL+'/#/board/'+card.boardId+'/card/'+resData.activity.card_id; this.showSuccessMessage(`Archived Card: ${resData.activity.card_id}-${resData.activity.card_name}`, card_url); return 0; } showCardMembersAsString(members: RestyaboardMember[]): string { if (!members || members.length == 0) { return ""; } return members.map(member => member.initials).join(", "); } showCardLabelsAsString(labels: RestyaboardLabel[]): string { if (!labels || labels.length == 0) { return ""; } return labels.map(label => label.name).join(", "); } showChecklistsAsMarkdown(checklists: RestyaboardChecklist[]): string { if (!checklists || checklists.length == 0) { return ""; } let checklistMarkdown: string = ""; checklists.map(checklist => { checklistMarkdown += `\n> ${checklist.name}\n\n`; checklist.checklists_items .sort((checkItem1: CheckItem, checkItem2: CheckItem) => checkItem1.position - checkItem2.position) .map((checkItem: CheckItem) => { checklistMarkdown += checkItem.is_completed == "1" ? `✅ ~~${checkItem.name}~~ \n` : `🔳 ${checkItem.name} \n`; }); }); return checklistMarkdown; } showCommentsAsMarkdown(comments: RestyaboardActionComment[]): string { if (!comments || comments.length == 0) { return ""; } let commentsMarkdown: string = ""; comments.map((comment: RestyaboardActionComment) => { const date = new Date(comment.created); const dateString = `${date.toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric" })}`; const timeString = `${date.toLocaleString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: true, })}`; let depth_string: string = ""; for(let i=0; i<=comment.depth; i++){ depth_string += '>'; } commentsMarkdown += `\n${depth_string} ${comment.full_name} - ${dateString} at ${timeString} ${ comment.revisions ? "(edited)" : "" } \n`; commentsMarkdown += `\n\n${comment.comment}\n\n`; }); return commentsMarkdown; } showMarkdownDecorated(header: string, content: string | undefined): string { return content ? `## **\`${header}\`** \n${content}\n\n--- \n` : ""; } async showCard(card: RestyaboardCard): Promise<void> { if (!card) { vscode.window.showErrorMessage("No card selected or invalid card."); return; } let Cardcomments: any = await this.getcardComments(card.id, card.board_id, card.list_id); const cardMembers: string = this.showCardMembersAsString(card.cards_users); const cardLabels: string = this.showCardLabelsAsString(card.cards_labels); const checklistItems: string = this.showChecklistsAsMarkdown(card.cards_checklists); const commentItems: string = this.showCommentsAsMarkdown(Cardcomments); const cardCoverImageUrl = !!card.attachments && card.attachments.length > 0 ? card.attachments[0].url : ""; const cardContentAndHeaders = [ { header: "URL", content: card.url }, { header: "Title", content: card.name }, { header: "Board Name", content: card.board_name }, { header: "List Name", content: card.list_name }, { header: "Members", content: cardMembers }, { header: "Labels", content: cardLabels }, { header: "Description", content: card.description }, { header: "Checklists", content: checklistItems }, { header: "Comments", content: commentItems }, ]; if(card.due_date){ cardContentAndHeaders.push( { header: "Due Date", content: card.due_date }); } let cardContent: string = ""; // cardContent += "| Board Name | List Name |\n| ------------- | ------------- |\n| "+card.board_name+" | "+card.list_name+" |\n"; cardContentAndHeaders.map(({ header, content }) => { cardContent += this.showMarkdownDecorated(header, content); }); cardContent += cardCoverImageUrl ? `<img src="${cardCoverImageUrl}" alt="Image not found" />` : ""; // Write temp markdown file at user's vs code default settings directory writeFile(this.tempRestyaboardFile, cardContent, err => { if (err) { vscode.window.showErrorMessage(`Error writing to temp file: ${err}`); } console.info(`✍ Writing to file: ${this.tempRestyaboardFile}`); }); // open markdown file and preview view let viewColumn: vscode.ViewColumn = vscode.workspace.getConfiguration(SETTING_PREFIX, null).get(SETTING_CONFIG.VIEW_COLUMN) || SETTING_CONFIG.DEFAULT_VIEW_COLUMN; if (!(VSCODE_VIEW_COLUMN.indexOf(viewColumn) > -1)) { vscode.window.showInformationMessage(`Invalid ${SETTING_PREFIX}.viewColumn ${viewColumn} specified`); viewColumn = SETTING_CONFIG.DEFAULT_VIEW_COLUMN; } const doc = await vscode.workspace.openTextDocument(this.tempRestyaboardFile); await vscode.window.showTextDocument(doc, viewColumn, false); await vscode.commands.executeCommand("markdown.showPreview"); vscode.commands.executeCommand("markdown.preview.toggleLock"); } } export function removeTempRestyaboardFile() { const userDataFolder = new UserDataFolder(); const tempRestyaboardFile = userDataFolder.getPathCodeSettings() + TEMP_RESTYABOARD_FILE_NAME; unlink(tempRestyaboardFile, err => { if (err) throw err; console.info(`❌ Deleted file: ${tempRestyaboardFile}`); }); }