1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-08 15:06:08 +02:00
focalboard/webapp/src/octoClient.ts
Jesús Espino f915a20c64
Adding the new blocks based editor (#3825)
* Working in the new content block editor

* Moving blocksEditor content block into its own component

* Initial integration with quick development flow

* More WIP

* Adding drag and drop support with server side help

* Some extra work around the styles

* Adding image support

* Adding video and attachments, and fixing edit

* Putting everything behind a feature flag

* Adding support for download attachments

* Fixing compilation error

* Fixing linter errors

* Fixing javascript tests

* Fixing a typescript error

* Moving the move block to an action with undo support

* Fixing ci

* Fixing post merge errors

* Moving to more specific content-blocks api

* Apply suggestions from code review

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>

* Fixing the behavior of certain blocks

* Fixing linter error

* Fixing javascript linter errors

* Adding permission testing for the new move content block api

* Adding some unit tests

* Improving a bit the tests

* Adding more unit tests to the backend

* Fixed PR suggestion

* Adding h1, h2 and h3 tests

* Adding image tests

* Adding video tests

* Adding attachment tests

* Adding quote block tests

* Adding divider tests

* Adding checkbox tests

* Adding list item block tests

* Adding text block tests

* Reorganizing a bit the code to support deveditor eagain

* Fixing dark theme on editor view

* Fixing linter errors

* Fixing tests and removing unneeded data-testid

* Adding root input tests

* Fixing some merge problems

* Fixing text/text.test.tsx test

* Adding more unit tests to the blocks editor

* Fix linter error

* Adding blocksEditor tests

* Fixing linter errors

* Adding tests for blockContent

* Update webapp/src/components/blocksEditor/blockContent.test.tsx

Fix linter warning

* Update webapp/src/components/blocksEditor/blockContent.test.tsx

Fix linter warning

* Update webapp/src/components/blocksEditor/blockContent.test.tsx

Fix linter error

* Fixing test

* Removing unneeded TODO

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
2022-11-15 16:59:07 +01:00

971 lines
34 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Block, BlockPatch, FileInfo} from './blocks/block'
import {Board, BoardsAndBlocks, BoardsAndBlocksPatch, BoardPatch, BoardMember} from './blocks/board'
import {ISharing} from './blocks/sharing'
import {OctoUtils} from './octoUtils'
import {IUser, UserConfigPatch, UserPreference} from './user'
import {Utils} from './utils'
import {ClientConfig} from './config/clientConfig'
import {UserSettings} from './userSettings'
import {Category, CategoryBoards} from './store/sidebar'
import {Channel} from './store/channels'
import {Team} from './store/teams'
import {Subscription} from './wsclient'
import {PrepareOnboardingResponse} from './onboardingTour'
import {Constants} from './constants'
import {BoardsCloudLimits} from './boardsCloudLimits'
import {TopBoardResponse} from './insights'
import {BoardSiteStatistics} from './statistics'
//
// OctoClient is the client interface to the server APIs
//
class OctoClient {
readonly serverUrl: string | undefined
private logged = false
// this need to be a function rather than a const because
// one of the global variable (`window.baseURL`) is set at runtime
// after the first instance of OctoClient is created.
// Avoiding the race condition becomes more complex than making
// the base URL dynamic though a function
private getBaseURL(): string {
const baseURL = (this.serverUrl || Utils.getBaseURL(true)).replace(/\/$/, '')
// Logging this for debugging.
// Logging just once to avoid log noise.
if (!this.logged) {
Utils.log(`OctoClient baseURL: ${baseURL}`)
this.logged = true
}
return baseURL
}
get token(): string {
return localStorage.getItem('focalboardSessionId') || ''
}
set token(value: string) {
localStorage.setItem('focalboardSessionId', value)
}
constructor(serverUrl?: string, public teamId = Constants.globalTeamId) {
this.serverUrl = serverUrl
}
private async getJson<T>(response: Response, defaultValue: T): Promise<T> {
// The server may return null or malformed json
try {
const value = await response.json()
return value || defaultValue
} catch {
return defaultValue
}
}
async login(username: string, password: string): Promise<boolean> {
const path = '/api/v2/login'
const body = JSON.stringify({username, password, type: 'normal'})
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
body,
})
if (response.status !== 200) {
return false
}
const responseJson = (await this.getJson(response, {})) as {token?: string}
if (responseJson.token) {
localStorage.setItem('focalboardSessionId', responseJson.token)
return true
}
return false
}
async logout(): Promise<boolean> {
const path = '/api/v2/logout'
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
})
localStorage.removeItem('focalboardSessionId')
if (response.status !== 200) {
return false
}
return true
}
async getClientConfig(): Promise<ClientConfig | null> {
const path = '/api/v2/clientConfig'
const response = await fetch(this.getBaseURL() + path, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return null
}
const json = (await this.getJson(response, {})) as ClientConfig
return json
}
async register(email: string, username: string, password: string, token?: string): Promise<{code: number, json: {error?: string}}> {
const path = '/api/v2/register'
const body = JSON.stringify({email, username, password, token})
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
body,
})
const json = (await this.getJson(response, {})) as {error?: string}
return {code: response.status, json}
}
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<{code: number, json: {error?: string}}> {
const path = `/api/v2/users/${encodeURIComponent(userId)}/changepassword`
const body = JSON.stringify({oldPassword, newPassword})
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
body,
})
const json = (await this.getJson(response, {})) as {error?: string}
return {code: response.status, json}
}
private headers() {
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: this.token ? 'Bearer ' + this.token : '',
'X-Requested-With': 'XMLHttpRequest',
}
}
private teamPath(teamId?: string): string {
let teamIdToUse = teamId
if (!teamId) {
teamIdToUse = this.teamId === Constants.globalTeamId ? UserSettings.lastTeamId || this.teamId : this.teamId
}
return `/api/v2/teams/${teamIdToUse}`
}
private teamsPath(): string {
return '/api/v2/teams'
}
async getMe(): Promise<IUser | undefined> {
const path = '/api/v2/users/me'
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
const user = (await this.getJson(response, {})) as IUser
return user
}
async getMyBoardMemberships(): Promise<BoardMember[]> {
const path = '/api/v2/users/me/memberships'
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
const members = (await this.getJson(response, [])) as BoardMember[]
return members
}
async getUser(userId: string): Promise<IUser | undefined> {
const path = `/api/v2/users/${encodeURIComponent(userId)}`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
const user = (await this.getJson(response, {})) as IUser
return user
}
async getUsersList(userIds: string[]): Promise<IUser[] | []> {
const path = '/api/v2/users'
const body = JSON.stringify(userIds)
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'POST',
body,
})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as IUser[]
}
async getMyConfig(): Promise<UserPreference[] | undefined> {
const path = '/api/v2/users/me/config'
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'GET',
})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, [])) as UserPreference[]
}
async patchUserConfig(userID: string, patch: UserConfigPatch): Promise<UserPreference[] | undefined> {
const path = `/api/v2/users/${encodeURIComponent(userID)}/config`
const body = JSON.stringify(patch)
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'PUT',
body,
})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, {})) as UserPreference[]
}
async exportBoardArchive(boardID: string): Promise<Response> {
const path = `/api/v2/boards/${boardID}/archive/export`
return fetch(this.getBaseURL() + path, {headers: this.headers()})
}
async exportFullArchive(teamID: string): Promise<Response> {
const path = `/api/v2/teams/${teamID}/archive/export`
return fetch(this.getBaseURL() + path, {headers: this.headers()})
}
async importFullArchive(file: File): Promise<Response> {
const formData = new FormData()
formData.append('file', file)
const headers = this.headers() as Record<string, string>
// TIPTIP: Leave out Content-Type here, it will be automatically set by the browser
delete headers['Content-Type']
return fetch(this.getBaseURL() + this.teamPath() + '/archive/import', {
method: 'POST',
headers,
body: formData,
})
}
async getBlocksWithParent(parentId: string, type?: string): Promise<Block[]> {
let path: string
if (type) {
path = this.teamPath() + `/blocks?parent_id=${encodeURIComponent(parentId)}&type=${encodeURIComponent(type)}`
} else {
path = this.teamPath() + `/blocks?parent_id=${encodeURIComponent(parentId)}`
}
return this.getBlocksWithPath(path)
}
async getBlocksWithType(type: string): Promise<Block[]> {
const path = this.teamPath() + `/blocks?type=${encodeURIComponent(type)}`
return this.getBlocksWithPath(path)
}
async getBlocksWithBlockID(blockID: string, boardID: string, optionalReadToken?: string): Promise<Block[]> {
let path = `/api/v2/boards/${boardID}/blocks?block_id=${blockID}`
const readToken = optionalReadToken || Utils.getReadToken()
if (readToken) {
path += `&read_token=${readToken}`
}
return this.getBlocksWithPath(path)
}
async getAllBlocks(boardID: string): Promise<Block[]> {
let path = `/api/v2/boards/${boardID}/blocks?all=true`
const readToken = Utils.getReadToken()
if (readToken) {
path += `&read_token=${readToken}`
}
return this.getBlocksWithPath(path)
}
private async getBlocksWithPath(path: string): Promise<Block[]> {
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
const blocks = (await this.getJson(response, [])) as Block[]
return this.fixBlocks(blocks)
}
private async getBoardsWithPath(path: string): Promise<Board[]> {
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
const boards = (await this.getJson(response, [])) as Board[]
return boards
}
private async getBoardMembersWithPath(path: string): Promise<BoardMember[]> {
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
const boardMembers = (await this.getJson(response, [])) as BoardMember[]
return boardMembers
}
fixBlocks(blocks: Block[]): Block[] {
if (!blocks) {
return []
}
// Hydrate is important, as it ensures that each block is complete to the current model
const fixedBlocks = OctoUtils.hydrateBlocks(blocks)
return fixedBlocks
}
async patchBlock(boardId: string, blockId: string, blockPatch: BlockPatch): Promise<Response> {
Utils.log(`patchBlock: ${blockId} block`)
const body = JSON.stringify(blockPatch)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}/blocks/${blockId}`, {
method: 'PATCH',
headers: this.headers(),
body,
})
}
async patchBlocks(blocks: Block[], blockPatches: BlockPatch[]): Promise<Response> {
Utils.log(`patchBlocks: ${blocks.length} blocks`)
const blockIds = blocks.map((block) => block.id)
const body = JSON.stringify({block_ids: blockIds, block_patches: blockPatches})
const path = this.getBaseURL() + this.teamPath() + '/blocks'
const response = fetch(path, {
method: 'PATCH',
headers: this.headers(),
body,
})
return response
}
async deleteBlock(boardId: string, blockId: string): Promise<Response> {
Utils.log(`deleteBlock: ${blockId} on board ${boardId}`)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}/blocks/${encodeURIComponent(blockId)}`, {
method: 'DELETE',
headers: this.headers(),
})
}
async undeleteBlock(boardId: string, blockId: string): Promise<Response> {
Utils.log(`undeleteBlock: ${blockId}`)
return fetch(`${this.getBaseURL()}/api/v2/boards/${encodeURIComponent(boardId)}/blocks/${encodeURIComponent(blockId)}/undelete`, {
method: 'POST',
headers: this.headers(),
})
}
async undeleteBoard(boardId: string): Promise<Response> {
Utils.log(`undeleteBoard: ${boardId}`)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}/undelete`, {
method: 'POST',
headers: this.headers(),
})
}
async followBlock(blockId: string, blockType: string, userId: string): Promise<Response> {
const body: Subscription = {
blockType,
blockId,
subscriberType: 'user',
subscriberId: userId,
}
return fetch(this.getBaseURL() + '/api/v2/subscriptions', {
method: 'POST',
headers: this.headers(),
body: JSON.stringify(body),
})
}
async unfollowBlock(blockId: string, blockType: string, userId: string): Promise<Response> {
return fetch(this.getBaseURL() + `/api/v2/subscriptions/${blockId}/${userId}`, {
method: 'DELETE',
headers: this.headers(),
})
}
async insertBlock(boardId: string, block: Block): Promise<Response> {
return this.insertBlocks(boardId, [block])
}
async insertBlocks(boardId: string, blocks: Block[], sourceBoardID?: string): Promise<Response> {
Utils.log(`insertBlocks: ${blocks.length} blocks(s) on board ${boardId}`)
blocks.forEach((block) => {
Utils.log(`\t ${block.type}, ${block.id}, ${block.title?.substr(0, 50) || ''}`)
})
const body = JSON.stringify(blocks)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}/blocks` + (sourceBoardID ? `?sourceBoardID=${encodeURIComponent(sourceBoardID)}` : ''), {
method: 'POST',
headers: this.headers(),
body,
})
}
async createBoardsAndBlocks(bab: BoardsAndBlocks): Promise<Response> {
Utils.log(`createBoardsAndBlocks: ${bab.boards.length} board(s) ${bab.blocks.length} block(s)`)
bab.boards.forEach((board: Board) => {
Utils.log(`\t Board ${board.id}, ${board.type}, ${board.title?.substr(0, 50) || ''}`)
})
bab.blocks.forEach((block: Block) => {
Utils.log(`\t Block ${block.id}, ${block.type}, ${block.title?.substr(0, 50) || ''}`)
})
const body = JSON.stringify(bab)
return fetch(this.getBaseURL() + '/api/v2/boards-and-blocks', {
method: 'POST',
headers: this.headers(),
body,
})
}
async deleteBoardsAndBlocks(boardIds: string[], blockIds: string[]): Promise<Response> {
Utils.log(`deleteBoardsAndBlocks: ${boardIds.length} board(s) ${blockIds.length} block(s)`)
Utils.log(`\t Boards ${boardIds.join(', ')}`)
Utils.log(`\t Blocks ${blockIds.join(', ')}`)
const body = JSON.stringify({boards: boardIds, blocks: blockIds})
return fetch(this.getBaseURL() + '/api/v2/boards-and-blocks', {
method: 'DELETE',
headers: this.headers(),
body,
})
}
// BoardMember
async createBoardMember(member: Partial<BoardMember>): Promise<BoardMember|undefined> {
Utils.log(`createBoardMember: user ${member.userId} and board ${member.boardId}`)
const body = JSON.stringify(member)
const response = await fetch(this.getBaseURL() + `/api/v2/boards/${member.boardId}/members`, {
method: 'POST',
headers: this.headers(),
body,
})
if (response.status !== 200) {
return undefined
}
return this.getJson<BoardMember>(response, {} as BoardMember)
}
async joinBoard(boardId: string): Promise<BoardMember|undefined> {
Utils.log(`joinBoard: board ${boardId}`)
const response = await fetch(this.getBaseURL() + `/api/v2/boards/${boardId}/join`, {
method: 'POST',
headers: this.headers(),
})
if (response.status !== 200) {
return undefined
}
return this.getJson<BoardMember>(response, {} as BoardMember)
}
async updateBoardMember(member: BoardMember): Promise<Response> {
Utils.log(`udpateBoardMember: user ${member.userId} and board ${member.boardId}`)
const body = JSON.stringify(member)
return fetch(this.getBaseURL() + `/api/v2/boards/${member.boardId}/members/${member.userId}`, {
method: 'PUT',
headers: this.headers(),
body,
})
}
async deleteBoardMember(member: BoardMember): Promise<Response> {
Utils.log(`deleteBoardMember: user ${member.userId} and board ${member.boardId}`)
return fetch(this.getBaseURL() + `/api/v2/boards/${member.boardId}/members/${member.userId}`, {
method: 'DELETE',
headers: this.headers(),
})
}
async patchBoardsAndBlocks(babp: BoardsAndBlocksPatch): Promise<Response> {
Utils.log(`patchBoardsAndBlocks: ${babp.boardIDs.length} board(s) ${babp.blockIDs.length} block(s)`)
Utils.log(`\t Board ${babp.boardIDs.join(', ')}`)
Utils.log(`\t Blocks ${babp.blockIDs.join(', ')}`)
const body = JSON.stringify(babp)
return fetch(this.getBaseURL() + '/api/v2/boards-and-blocks', {
method: 'PATCH',
headers: this.headers(),
body,
})
}
// Sharing
async getSharing(boardID: string): Promise<ISharing | undefined> {
const path = `/api/v2/boards/${boardID}/sharing`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
return this.getJson(response, undefined)
}
async setSharing(boardID: string, sharing: ISharing): Promise<boolean> {
const path = `/api/v2/boards/${boardID}/sharing`
const body = JSON.stringify(sharing)
const response = await fetch(
this.getBaseURL() + path,
{
method: 'POST',
headers: this.headers(),
body,
},
)
if (response.status !== 200) {
return false
}
return true
}
async regenerateTeamSignupToken(): Promise<void> {
const path = this.teamPath() + '/regenerate_signup_token'
await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
})
}
// Files
// Returns fileId of uploaded file, or undefined on failure
async uploadFile(rootID: string, file: File): Promise<string | undefined> {
// IMPORTANT: We need to post the image as a form. The browser will convert this to a application/x-www-form-urlencoded POST
const formData = new FormData()
formData.append('file', file)
try {
const headers = this.headers() as Record<string, string>
// TIPTIP: Leave out Content-Type here, it will be automatically set by the browser
delete headers['Content-Type']
const response = await fetch(this.getBaseURL() + this.teamPath() + '/' + rootID + '/files', {
method: 'POST',
headers,
body: formData,
})
if (response.status !== 200) {
return undefined
}
try {
const text = await response.text()
Utils.log(`uploadFile response: ${text}`)
const json = JSON.parse(text)
return json.fileId
} catch (e) {
Utils.logError(`uploadFile json ERROR: ${e}`)
}
} catch (e) {
Utils.logError(`uploadFile ERROR: ${e}`)
}
return undefined
}
async getFileAsDataUrl(boardId: string, fileId: string): Promise<FileInfo> {
let path = '/api/v2/files/teams/' + this.teamId + '/' + boardId + '/' + fileId
const readToken = Utils.getReadToken()
if (readToken) {
path += `?read_token=${readToken}`
}
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
let fileInfo: FileInfo = {}
if (response.status === 200) {
const blob = await response.blob()
fileInfo.url = URL.createObjectURL(blob)
} else if (response.status === 400) {
fileInfo = await this.getJson(response, {}) as FileInfo
}
return fileInfo
}
async getTeam(): Promise<Team | null> {
const path = this.teamPath()
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return null
}
return this.getJson(response, null)
}
async getTeams(): Promise<Team[]> {
const path = this.teamsPath()
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return this.getJson<Team[]>(response, [])
}
async getTeamUsers(excludeBots?: boolean): Promise<IUser[]> {
let path = this.teamPath() + '/users'
if (excludeBots) {
path += '?exclude_bots=true'
}
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as IUser[]
}
async searchTeamUsers(searchQuery: string, excludeBots?: boolean): Promise<IUser[]> {
let path = this.teamPath() + `/users?search=${searchQuery}`
if (excludeBots) {
path += '&exclude_bots=true'
}
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as IUser[]
}
async getTeamTemplates(teamId?: string): Promise<Board[]> {
const path = this.teamPath(teamId) + '/templates'
return this.getBoardsWithPath(path)
}
async getBoards(): Promise<Board[]> {
const path = this.teamPath() + '/boards'
return this.getBoardsWithPath(path)
}
async getBoard(boardID: string): Promise<Board | undefined> {
let path = `/api/v2/boards/${boardID}`
const readToken = Utils.getReadToken()
if (readToken) {
path += `?read_token=${readToken}`
}
const response = await fetch(this.getBaseURL() + path, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return undefined
}
return this.getJson<Board>(response, {} as Board)
}
async duplicateBoard(boardID: string, asTemplate: boolean, toTeam?: string): Promise<BoardsAndBlocks | undefined> {
let query = '?asTemplate=false'
if (asTemplate) {
query = '?asTemplate=true'
}
if (toTeam) {
query += `&toTeam=${encodeURIComponent(toTeam)}`
}
const path = `/api/v2/boards/${boardID}/duplicate${query}`
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
})
if (response.status !== 200) {
return undefined
}
return this.getJson<BoardsAndBlocks>(response, {} as BoardsAndBlocks)
}
async duplicateBlock(boardID: string, blockID: string, asTemplate: boolean): Promise<Block[] | undefined> {
let query = '?asTemplate=false'
if (asTemplate) {
query = '?asTemplate=true'
}
const path = `/api/v2/boards/${boardID}/blocks/${blockID}/duplicate${query}`
const response = await fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
})
if (response.status !== 200) {
return undefined
}
return this.getJson<Block[]>(response, [] as Block[])
}
async getBlocksForBoard(teamId: string, boardId: string): Promise<Board[]> {
const path = this.teamPath(teamId) + `/boards/${boardId}`
return this.getBoardsWithPath(path)
}
async getBoardMembers(teamId: string, boardId: string): Promise<BoardMember[]> {
const path = `/api/v2/boards/${boardId}/members`
return this.getBoardMembersWithPath(path)
}
async createBoard(board: Board): Promise<Response> {
Utils.log(`createBoard: ${board.title} [${board.type}]`)
return fetch(this.getBaseURL() + this.teamPath(board.teamId) + '/boards', {
method: 'POST',
headers: this.headers(),
body: JSON.stringify(board),
})
}
async patchBoard(boardId: string, boardPatch: BoardPatch): Promise<Response> {
Utils.log(`patchBoard: ${boardId} board`)
const body = JSON.stringify(boardPatch)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}`, {
method: 'PATCH',
headers: this.headers(),
body,
})
}
async deleteBoard(boardId: string): Promise<Response> {
Utils.log(`deleteBoard: ${boardId}`)
return fetch(`${this.getBaseURL()}/api/v2/boards/${boardId}`, {
method: 'DELETE',
headers: this.headers(),
})
}
async getSidebarCategories(teamID: string): Promise<CategoryBoards[]> {
const path = `/api/v2/teams/${teamID}/categories`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as CategoryBoards[]
}
async createSidebarCategory(category: Category): Promise<Response> {
const path = `/api/v2/teams/${category.teamID}/categories`
const body = JSON.stringify(category)
return fetch(this.getBaseURL() + path, {
method: 'POST',
headers: this.headers(),
body,
})
}
async deleteSidebarCategory(teamID: string, categoryID: string): Promise<Response> {
const url = `/api/v2/teams/${teamID}/categories/${categoryID}`
return fetch(this.getBaseURL() + url, {
method: 'DELETE',
headers: this.headers(),
})
}
async updateSidebarCategory(category: Category): Promise<Response> {
const path = `/api/v2/teams/${category.teamID}/categories/${category.id}`
const body = JSON.stringify(category)
return fetch(this.getBaseURL() + path, {
method: 'PUT',
headers: this.headers(),
body,
})
}
async moveBoardToCategory(teamID: string, boardID: string, toCategoryID: string, fromCategoryID: string): Promise<Response> {
const url = `/api/v2/teams/${teamID}/categories/${toCategoryID || '0'}/boards/${boardID}`
const payload = {
fromCategoryID,
}
const body = JSON.stringify(payload)
return fetch(this.getBaseURL() + url, {
method: 'POST',
headers: this.headers(),
body,
})
}
async search(teamID: string, query: string): Promise<Board[]> {
const url = `${this.teamPath(teamID)}/boards/search?q=${encodeURIComponent(query)}`
const response = await fetch(this.getBaseURL() + url, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as Board[]
}
async searchLinkableBoards(teamID: string, query: string): Promise<Board[]> {
const url = `${this.teamPath(teamID)}/boards/search/linkable?q=${encodeURIComponent(query)}`
const response = await fetch(this.getBaseURL() + url, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as Board[]
}
async searchAll(query: string): Promise<Board[]> {
const url = `/api/v2/boards/search?q=${encodeURIComponent(query)}`
const response = await fetch(this.getBaseURL() + url, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as Board[]
}
async getUserBlockSubscriptions(userId: string): Promise<Subscription[]> {
const path = `/api/v2/subscriptions/${userId}`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return []
}
return (await this.getJson(response, [])) as Subscription[]
}
async searchUserChannels(teamId: string, searchQuery: string): Promise<Channel[] | undefined> {
const path = `/api/v2/teams/${teamId}/channels?search=${searchQuery}`
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'GET',
})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, [])) as Channel[]
}
async getChannel(teamId: string, channelId: string): Promise<Channel | undefined> {
const path = `/api/v2/teams/${teamId}/channels/${channelId}`
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'GET',
})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, {})) as Channel
}
// onboarding
async prepareOnboarding(teamId: string): Promise<PrepareOnboardingResponse | undefined> {
const path = `/api/v2/teams/${teamId}/onboard`
const response = await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'POST',
})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, {})) as PrepareOnboardingResponse
}
async notifyAdminUpgrade(): Promise<void> {
const path = `${this.teamPath()}/notifyadminupgrade`
await fetch(this.getBaseURL() + path, {
headers: this.headers(),
method: 'POST',
})
}
async getBoardsCloudLimits(): Promise<BoardsCloudLimits | undefined> {
const path = '/api/v2/limits'
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
const limits = (await this.getJson(response, {})) as BoardsCloudLimits
Utils.log(`Cloud limits: cards=${limits.cards} views=${limits.views}`)
return limits
}
async getSiteStatistics(): Promise<BoardSiteStatistics | undefined> {
const path = '/api/v2/statistics'
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
const stats = (await this.getJson(response, {})) as BoardSiteStatistics
Utils.log(`Site Statistics: cards=${stats.card_count} boards=${stats.board_count}`)
return stats
}
// insights
async getMyTopBoards(timeRange: string, page: number, perPage: number, teamId: string): Promise<TopBoardResponse | undefined> {
const path = `/api/v2/users/me/boards/insights?time_range=${timeRange}&page=${page}&per_page=${perPage}&team_id=${teamId}`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, {})) as TopBoardResponse
}
async getTeamTopBoards(timeRange: string, page: number, perPage: number, teamId: string): Promise<TopBoardResponse | undefined> {
const path = `/api/v2/teams/${teamId}/boards/insights?time_range=${timeRange}&page=${page}&per_page=${perPage}`
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
return undefined
}
return (await this.getJson(response, {})) as TopBoardResponse
}
async moveBlockTo(blockId: string, where: 'before'|'after', dstBlockId: string): Promise<Response> {
return fetch(`${this.getBaseURL()}/api/v2/content-blocks/${blockId}/moveto/${where}/${dstBlockId}`, {
method: 'POST',
headers: this.headers(),
body: '{}',
})
}
}
const octoClient = new OctoClient()
export {OctoClient}
export default octoClient