Unfurl Focalboard Link (#1081)
This commit is contained in:
parent
8666bc833a
commit
eed1f86c15
17 changed files with 997 additions and 15 deletions
|
@ -7,7 +7,7 @@ replace github.com/mattermost/focalboard/server => ../server
|
|||
require (
|
||||
github.com/mattermost/focalboard/server v0.0.0-20210525112228-f43e4028dbdc
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.20
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20211013145127-6521b0dfe3e5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
|
@ -19,9 +22,19 @@ import (
|
|||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/markdown"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
type BoardsEmbed struct {
|
||||
OriginalPath string `json:"originalPath"`
|
||||
WorkspaceID string `json:"workspaceID"`
|
||||
ViewID string `json:"viewID"`
|
||||
BoardID string `json:"boardID"`
|
||||
CardID string `json:"cardID"`
|
||||
ReadToken string `json:"readToken,omitempty"`
|
||||
}
|
||||
|
||||
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
|
||||
type Plugin struct {
|
||||
plugin.MattermostPlugin
|
||||
|
@ -219,3 +232,130 @@ func defaultLoggingConfig() string {
|
|||
}
|
||||
}`
|
||||
}
|
||||
|
||||
func (p *Plugin) MessageWillBePosted(_ *plugin.Context, post *mmModel.Post) (*mmModel.Post, string) {
|
||||
return postWithBoardsEmbed(post, p.API.GetConfig().FeatureFlags.BoardsUnfurl), ""
|
||||
}
|
||||
|
||||
func (p *Plugin) MessageWillBeUpdated(_ *plugin.Context, newPost, _ *mmModel.Post) (*mmModel.Post, string) {
|
||||
return postWithBoardsEmbed(newPost, p.API.GetConfig().FeatureFlags.BoardsUnfurl), ""
|
||||
}
|
||||
|
||||
func postWithBoardsEmbed(post *mmModel.Post, showBoardsUnfurl bool) *mmModel.Post {
|
||||
if _, ok := post.GetProps()["boards"]; ok {
|
||||
post.AddProp("boards", nil)
|
||||
}
|
||||
|
||||
if !showBoardsUnfurl {
|
||||
return post
|
||||
}
|
||||
|
||||
firstLink := getFirstLink(post.Message)
|
||||
|
||||
if firstLink == "" {
|
||||
return post
|
||||
}
|
||||
|
||||
u, err := url.Parse(firstLink)
|
||||
|
||||
if err != nil {
|
||||
return post
|
||||
}
|
||||
|
||||
// Trim away the first / because otherwise after we split the string, the first element in the array is a empty element
|
||||
urlPath := u.Path
|
||||
if strings.HasPrefix(urlPath, "/") {
|
||||
urlPath = u.Path[1:]
|
||||
}
|
||||
pathSplit := strings.Split(strings.ToLower(urlPath), "/")
|
||||
queryParams := u.Query()
|
||||
|
||||
if len(pathSplit) == 0 {
|
||||
return post
|
||||
}
|
||||
|
||||
workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit)
|
||||
|
||||
if workspaceID != "" && boardID != "" && viewID != "" && cardID != "" {
|
||||
b, _ := json.Marshal(BoardsEmbed{
|
||||
WorkspaceID: workspaceID,
|
||||
BoardID: boardID,
|
||||
ViewID: viewID,
|
||||
CardID: cardID,
|
||||
ReadToken: queryParams.Get("r"),
|
||||
OriginalPath: u.RequestURI(),
|
||||
})
|
||||
|
||||
BoardsPostEmbed := &mmModel.PostEmbed{
|
||||
Type: mmModel.PostEmbedBoards,
|
||||
Data: string(b),
|
||||
}
|
||||
|
||||
if post.Metadata == nil {
|
||||
post.Metadata = &mmModel.PostMetadata{}
|
||||
}
|
||||
|
||||
post.Metadata.Embeds = []*mmModel.PostEmbed{BoardsPostEmbed}
|
||||
post.AddProp("boards", string(b))
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
func getFirstLink(str string) string {
|
||||
firstLink := ""
|
||||
|
||||
markdown.Inspect(str, func(blockOrInline interface{}) bool {
|
||||
if _, ok := blockOrInline.(*markdown.Autolink); ok {
|
||||
if link := blockOrInline.(*markdown.Autolink).Destination(); firstLink == "" {
|
||||
firstLink = link
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return firstLink
|
||||
}
|
||||
|
||||
func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardID string) {
|
||||
// The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths
|
||||
index := -1
|
||||
for i := 0; i < len(pathArray); i++ {
|
||||
if pathArray[i] == "boards" || pathArray[i] == "plugins" {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return workspaceID, boardID, viewID, cardID
|
||||
}
|
||||
|
||||
// If at index, the parameter in the path is boards,
|
||||
// then we've copied this directly as logged in user of that board
|
||||
|
||||
// If at index, the parameter in the path is plugins,
|
||||
// then we've copied this from a shared board
|
||||
|
||||
// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/workspace/workspaceID/boardID/viewID/cardID
|
||||
|
||||
// For card links copied on a shared board, the path looks like
|
||||
// {...Mattermost Url}.../plugins/focalboard/workspace/workspaceID/shared/boardID/viewID/cardID?r=read_token
|
||||
|
||||
// This is a non-shared board card link
|
||||
if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "workspace" {
|
||||
workspaceID = pathArray[index+2]
|
||||
boardID = pathArray[index+3]
|
||||
viewID = pathArray[index+4]
|
||||
cardID = pathArray[index+5]
|
||||
} else if len(pathArray)-index == 8 && pathArray[index] == "plugins" &&
|
||||
pathArray[index+1] == "focalboard" &&
|
||||
pathArray[index+2] == "workspace" &&
|
||||
pathArray[index+4] == "shared" { // This is a shared board card link
|
||||
workspaceID = pathArray[index+3]
|
||||
boardID = pathArray[index+5]
|
||||
viewID = pathArray[index+6]
|
||||
cardID = pathArray[index+7]
|
||||
}
|
||||
return workspaceID, boardID, viewID, cardID
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ function blockList(line) {
|
|||
return line.startsWith('.focalboard-body') ||
|
||||
line.startsWith('.GlobalHeaderComponent') ||
|
||||
line.startsWith('.boards-rhs-icon') ||
|
||||
line.startsWith('.focalboard-plugin-root');
|
||||
line.startsWith('.focalboard-plugin-root') ||
|
||||
line.startsWith('.FocalboardUnfurl');
|
||||
}
|
||||
|
||||
module.exports = function loader(source) {
|
||||
|
|
17
mattermost-plugin/webapp/package-lock.json
generated
17
mattermost-plugin/webapp/package-lock.json
generated
|
@ -6,6 +6,7 @@
|
|||
"": {
|
||||
"dependencies": {
|
||||
"core-js": "3.12.1",
|
||||
"marked": ">=2.0.1",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react-intl": "^5.13.5",
|
||||
"react-router-dom": "5.2.0"
|
||||
|
@ -12235,6 +12236,17 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.6.tgz",
|
||||
"integrity": "sha512-a1hY8eqdP9JgmsaO0MYYhO9Li2nfY/5pAj+gWU5r41Lze6AV4Xty1cseLWDcOYimJnaVfQAomaA6NK+z2IyR+w==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/mattermost-redux": {
|
||||
"version": "5.33.1",
|
||||
"resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.33.1.tgz",
|
||||
|
@ -27341,6 +27353,11 @@
|
|||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-3.0.6.tgz",
|
||||
"integrity": "sha512-a1hY8eqdP9JgmsaO0MYYhO9Li2nfY/5pAj+gWU5r41Lze6AV4Xty1cseLWDcOYimJnaVfQAomaA6NK+z2IyR+w=="
|
||||
},
|
||||
"mattermost-redux": {
|
||||
"version": "5.33.1",
|
||||
"resolved": "https://registry.npmjs.org/mattermost-redux/-/mattermost-redux-5.33.1.tgz",
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.12.1",
|
||||
"marked": ">=2.0.1",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react-intl": "^5.13.5",
|
||||
"react-router-dom": "5.2.0"
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
.FocalboardUnfurl {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(61, 60, 64, 0.24) !important;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.08);
|
||||
width: 425px;
|
||||
text-decoration: none !important;
|
||||
color: inherit !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.information {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.card_title {
|
||||
color: #3D3C40;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.board_title {
|
||||
color: rgba(61, 60, 64, 0.56);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
border: 1px solid rgba(61, 60, 64, 0.16);
|
||||
border-radius: 4px;
|
||||
margin-top: 16px;
|
||||
height: 145px;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
margin-top: 16px;
|
||||
|
||||
.timestamp_properties {
|
||||
margin-left: 12px;
|
||||
max-width: 90%;
|
||||
|
||||
.properties {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
.remainder {
|
||||
color: rgba(61, 60, 64, 0.48);
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.property {
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
margin-right: 10px;
|
||||
max-width: 33%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
|
||||
&.propColorDefault {
|
||||
background-color: var(--prop-default);
|
||||
}
|
||||
|
||||
&.propColorGray {
|
||||
background-color: var(--prop-gray);
|
||||
}
|
||||
|
||||
&.propColorBrown {
|
||||
background-color: var(--prop-brown);
|
||||
}
|
||||
|
||||
&.propColorOrange {
|
||||
background-color: var(--prop-orange);
|
||||
}
|
||||
|
||||
&.propColorYellow {
|
||||
background-color: var(--prop-yellow);
|
||||
}
|
||||
|
||||
&.propColorGreen {
|
||||
background-color: var(--prop-green);
|
||||
}
|
||||
|
||||
&.propColorBlue {
|
||||
background-color: var(--prop-blue);
|
||||
}
|
||||
|
||||
&.propColorPurple {
|
||||
background-color: var(--prop-purple);
|
||||
}
|
||||
|
||||
&.propColorPink {
|
||||
background-color: var(--prop-pink);
|
||||
}
|
||||
|
||||
&.propColorRed {
|
||||
background-color: var(--prop-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {IntlProvider, FormattedMessage} from 'react-intl'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {GlobalState} from 'mattermost-redux/types/store'
|
||||
import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n'
|
||||
|
||||
import {getMessages} from './../../../../../webapp/src/i18n'
|
||||
import {Utils} from './../../../../../webapp/src/utils'
|
||||
import {Card} from './../../../../../webapp/src/blocks/card'
|
||||
import {Board} from './../../../../../webapp/src/blocks/board'
|
||||
import {ContentBlock} from './../../../../../webapp/src/blocks/contentBlock'
|
||||
import octoClient from './../../../../../webapp/src/octoClient'
|
||||
|
||||
const Avatar = (window as any).Components.Avatar
|
||||
const Timestamp = (window as any).Components.Timestamp
|
||||
const imageURLForUser = (window as any).Components.imageURLForUser
|
||||
|
||||
import './boardsUnfurl.scss'
|
||||
|
||||
type Props = {
|
||||
embed: {
|
||||
data: string,
|
||||
},
|
||||
locale: string,
|
||||
}
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const locale = getCurrentUserLocale(state)
|
||||
|
||||
return {
|
||||
locale,
|
||||
}
|
||||
}
|
||||
|
||||
const BoardsUnfurl = (props: Props): JSX.Element => {
|
||||
if (!props.embed || !props.embed.data) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const {embed, locale} = props
|
||||
const focalboardInformation = JSON.parse(embed.data)
|
||||
const {workspaceID, cardID, boardID, readToken, originalPath} = focalboardInformation
|
||||
const baseURL = window.location.origin
|
||||
|
||||
if (!workspaceID || !cardID || !boardID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const [card, setCard] = useState<Card>()
|
||||
const [content, setContent] = useState<ContentBlock>()
|
||||
const [board, setBoard] = useState<Board>()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const [cards, boards] = await Promise.all(
|
||||
[
|
||||
octoClient.getBlocksWithBlockID(cardID, workspaceID, readToken),
|
||||
octoClient.getBlocksWithBlockID(boardID, workspaceID, readToken),
|
||||
],
|
||||
)
|
||||
const [firstCard] = cards as Card[]
|
||||
const [firstBoard] = boards as Board[]
|
||||
if (!firstCard || !firstBoard) {
|
||||
return null
|
||||
}
|
||||
setCard(firstCard)
|
||||
setBoard(firstBoard)
|
||||
|
||||
if (firstCard.fields.contentOrder.length) {
|
||||
let [firstContentBlockID] = firstCard.fields?.contentOrder
|
||||
|
||||
if (Array.isArray(firstContentBlockID)) {
|
||||
[firstContentBlockID] = firstContentBlockID
|
||||
}
|
||||
|
||||
const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, workspaceID, readToken) as ContentBlock[]
|
||||
const [firstContentBlock] = contentBlock
|
||||
if (!firstContentBlock) {
|
||||
return null
|
||||
}
|
||||
setContent(firstContentBlock)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
fetchData()
|
||||
}, [originalPath])
|
||||
|
||||
if (!card || !board) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const propertyKeyArray = Object.keys(card.fields.properties)
|
||||
const propertyValueArray = Object.values(card.fields.properties)
|
||||
const options = board.fields.cardProperties
|
||||
const propertiesToDisplay: Array<Record<string, string>> = []
|
||||
|
||||
// We will just display the first 3 or less select/multi-select properties and do a +n for remainder if any remainder
|
||||
if (propertyKeyArray.length > 0) {
|
||||
for (let i = 0; i < propertyKeyArray.length && propertiesToDisplay.length < 3; i++) {
|
||||
const keyToLookUp = propertyKeyArray[i]
|
||||
const correspondingOption = options.find((option) => option.id === keyToLookUp)
|
||||
|
||||
if (!correspondingOption) {
|
||||
continue
|
||||
}
|
||||
|
||||
let valueToLookUp = propertyValueArray[i]
|
||||
if (Array.isArray(valueToLookUp)) {
|
||||
valueToLookUp = valueToLookUp[0]
|
||||
}
|
||||
|
||||
const optionSelected = correspondingOption.options.find((option) => option.id === valueToLookUp)
|
||||
|
||||
if (!optionSelected) {
|
||||
continue
|
||||
}
|
||||
|
||||
propertiesToDisplay.push({optionName: correspondingOption.name, optionValue: optionSelected.value, optionValueColour: optionSelected.color})
|
||||
}
|
||||
}
|
||||
const remainder = propertyKeyArray.length - propertiesToDisplay.length
|
||||
const html: string = Utils.htmlFromMarkdown(content?.title || '')
|
||||
return (
|
||||
<IntlProvider
|
||||
messages={getMessages(locale)}
|
||||
locale={locale}
|
||||
>
|
||||
<a
|
||||
className='FocalboardUnfurl'
|
||||
href={`${baseURL}${originalPath}`}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
|
||||
{/* Header of the Card*/}
|
||||
<div className='header'>
|
||||
<span className='icon'>{card.fields?.icon}</span>
|
||||
<div className='information'>
|
||||
<span className='card_title'>{card.title}</span>
|
||||
<span className='board_title'>{board.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body of the Card*/}
|
||||
{html !== '' &&
|
||||
<div className='body'>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* Footer of the Card*/}
|
||||
<div className='footer'>
|
||||
<div className='avatar'>
|
||||
<Avatar
|
||||
size={'md'}
|
||||
url={imageURLForUser(card.createdBy)}
|
||||
className={'avatar-post-preview'}
|
||||
/>
|
||||
</div>
|
||||
<div className='timestamp_properties'>
|
||||
<div className='properties'>
|
||||
{propertiesToDisplay.map((property) => (
|
||||
<div
|
||||
key={property.optionValue}
|
||||
className={`property ${property.optionValueColour}`}
|
||||
title={`${property.optionName}`}
|
||||
>
|
||||
{property.optionValue}
|
||||
</div>
|
||||
))}
|
||||
{remainder > 0 &&
|
||||
<span className='remainder'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Remainder'
|
||||
defaultMessage='+{remainder} more'
|
||||
values={{
|
||||
remainder,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<span className='post-preview__time'>
|
||||
<FormattedMessage
|
||||
id='BoardsUnfurl.Updated'
|
||||
defaultMessage='Updated {time}'
|
||||
values={{
|
||||
time: (
|
||||
<Timestamp
|
||||
value={card.updateAt}
|
||||
units={[
|
||||
'now',
|
||||
'minute',
|
||||
'hour',
|
||||
'day',
|
||||
]}
|
||||
useTime={false}
|
||||
day={'numeric'}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(BoardsUnfurl)
|
|
@ -13,6 +13,8 @@ windowAny.baseURL = '/plugins/focalboard'
|
|||
windowAny.frontendBaseURL = '/boards'
|
||||
windowAny.isFocalboardPlugin = true
|
||||
|
||||
import {ClientConfig} from 'mattermost-redux/types/config'
|
||||
|
||||
import App from '../../../webapp/src/app'
|
||||
import store from '../../../webapp/src/store'
|
||||
import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader'
|
||||
|
@ -26,6 +28,7 @@ import '../../../webapp/src/styles/main.scss'
|
|||
import '../../../webapp/src/styles/labels.scss'
|
||||
import octoClient from '../../../webapp/src/octoClient'
|
||||
|
||||
import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl'
|
||||
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG} from './../../../webapp/src/wsclient'
|
||||
|
||||
import manifest from './manifest'
|
||||
|
@ -143,6 +146,10 @@ export default class Plugin {
|
|||
}
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboardWorkspace, '', 'Boards')
|
||||
this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent)
|
||||
|
||||
if (mmStore.getState().entities.general.config?.['FeatureFlagBoardsUnfurl' as keyof Partial<ClientConfig>] === 'true') {
|
||||
this.registry.registerPostWillRenderEmbedComponent((embed) => embed.type === 'boards', BoardsUnfurl, false)
|
||||
}
|
||||
} else {
|
||||
windowAny.frontendBaseURL = subpath + '/plug/focalboard'
|
||||
this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface PluginRegistry {
|
|||
registerProductRoute(route: string, component: React.ElementType)
|
||||
unregisterComponent(componentId: string)
|
||||
registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType)
|
||||
registerPostWillRenderEmbedComponent(match: (embed: {type: string, data: any}) => void, component: any, toggleable: boolean)
|
||||
registerWebSocketEventHandler(event: string, handler: (e: any) => void)
|
||||
unregisterWebSocketEventHandler(event: string)
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
declare module "mm-react-router-dom"
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
declare module 'mm-react-router-dom'
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
// See LICENSE.txt for license information.
|
||||
const exec = require('child_process').exec;
|
||||
|
||||
const webpack = require('webpack');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const webpack = require('webpack');
|
||||
|
||||
const tsTransformer = require('@formatjs/ts-transformer');
|
||||
|
||||
const PLUGIN_ID = require('../plugin.json').id;
|
||||
|
|
|
@ -247,7 +247,8 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||
parentID := query.Get("parent_id")
|
||||
blockType := query.Get("type")
|
||||
all := query.Get("all")
|
||||
container, err := a.getContainer(r)
|
||||
blockID := query.Get("block_id")
|
||||
container, err := a.getContainerAllowingReadTokenForBlock(r, blockID)
|
||||
if err != nil {
|
||||
a.noContainerErrorResponse(w, r.URL.Path, err)
|
||||
return
|
||||
|
@ -258,15 +259,27 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||
auditRec.AddMeta("parentID", parentID)
|
||||
auditRec.AddMeta("blockType", blockType)
|
||||
auditRec.AddMeta("all", all)
|
||||
auditRec.AddMeta("blockID", blockID)
|
||||
|
||||
var blocks []model.Block
|
||||
if all != "" {
|
||||
var block *model.Block
|
||||
switch {
|
||||
case all != "":
|
||||
blocks, err = a.app.GetAllBlocks(*container)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
case blockID != "":
|
||||
block, err = a.app.GetBlockWithID(*container, blockID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if block != nil {
|
||||
blocks = append(blocks, *block)
|
||||
}
|
||||
default:
|
||||
blocks, err = a.app.GetBlocks(*container, parentID, blockType)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
|
@ -277,6 +290,7 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||
a.logger.Debug("GetBlocks",
|
||||
mlog.String("parentID", parentID),
|
||||
mlog.String("blockType", blockType),
|
||||
mlog.String("blockID", blockID),
|
||||
mlog.Int("block_count", len(blocks)),
|
||||
)
|
||||
|
||||
|
|
|
@ -20,6 +20,10 @@ func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([
|
|||
return a.store.GetBlocksWithParent(c, parentID)
|
||||
}
|
||||
|
||||
func (a *App) GetBlockWithID(c store.Container, blockID string) (*model.Block, error) {
|
||||
return a.store.GetBlock(c, blockID)
|
||||
}
|
||||
|
||||
func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksWithRootID(c, rootID)
|
||||
}
|
||||
|
@ -101,6 +105,7 @@ func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model
|
|||
if levels >= 3 {
|
||||
return a.store.GetSubTree3(c, blockID)
|
||||
}
|
||||
|
||||
return a.store.GetSubTree2(c, blockID)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
"BoardComponent.no-property-title": "Items with an empty {property} property will go here. This column cannot be removed.",
|
||||
"BoardComponent.show": "Show",
|
||||
"BoardPage.syncFailed": "Board may be deleted or access revoked.",
|
||||
"BoardsUnfurl.Remainder": "+{remainder} more",
|
||||
"BoardsUnfurl.Updated": "Updated {time}",
|
||||
"CardDetail.add-content": "Add content",
|
||||
"CardDetail.add-icon": "Add icon",
|
||||
"CardDetail.add-property": "+ Add a property",
|
||||
|
@ -21,6 +23,8 @@
|
|||
"CardDialog.nocard": "This card doesn't exist or is inaccessible.",
|
||||
"CardDialog.copiedLink": "Copied!",
|
||||
"CardDialog.copyLink": "Copy link",
|
||||
"CardDialog.editing-template": "You're editing a template.",
|
||||
"CardDialog.nocard": "This card doesn't exist or is inaccessible.",
|
||||
"ColorOption.selectColor": "Select {color} Color",
|
||||
"Comment.delete": "Delete",
|
||||
"CommentsList.send": "Send",
|
||||
|
@ -46,7 +50,14 @@
|
|||
"Dialog.closeDialog": "Close dialog",
|
||||
"EditableDayPicker.today": "Today",
|
||||
"EmptyCenterPanel.no-content": "Add or select a board from the sidebar to get started.",
|
||||
"EmptyCenterPanel.workspace": "This is the workspace for:",
|
||||
"EmptyCenterPanel.plugin.choose-a-template": "Choose a template",
|
||||
"EmptyCenterPanel.plugin.empty-board": "Start with an Empty Board",
|
||||
"EmptyCenterPanel.plugin.end-message": "You can change the channel using the switcher in the sidebar.",
|
||||
"EmptyCenterPanel.plugin.new-template": "New template",
|
||||
"EmptyCenterPanel.plugin.no-content-description": "Add a board to the sidebar using any of the templates defined below or start from scratch.{lineBreak} Members of \"{workspaceName}\" will have access to boards created here.",
|
||||
"EmptyCenterPanel.plugin.no-content-or": "or",
|
||||
"EmptyCenterPanel.plugin.no-content-title": "Create a Board in {workspaceName}",
|
||||
"Error.mobileweb": "Mobile web support is currently in early beta. Not all functionality may be present.",
|
||||
"Error.websocket-closed": "Websocket connection closed, connection interrupted. If this persists, check your server or web proxy configuration.",
|
||||
"Filter.includes": "includes",
|
||||
"Filter.is-empty": "is empty",
|
||||
|
@ -54,17 +65,17 @@
|
|||
"Filter.not-includes": "doesn't include",
|
||||
"FilterComponent.add-filter": "+ Add filter",
|
||||
"FilterComponent.delete": "Delete",
|
||||
"GalleryCard.delete": "Delete",
|
||||
"GalleryCard.duplicate": "Duplicate",
|
||||
"GalleryCard.copiedLink": "Copied!",
|
||||
"GalleryCard.copyLink": "Copy link",
|
||||
"GroupBy.ungroup": "Ungroup",
|
||||
"GalleryCard.delete": "Delete",
|
||||
"GalleryCard.duplicate": "Duplicate",
|
||||
"General.BoardCount": "{count, plural, one {# Board} other {# Boards}}",
|
||||
"GroupBy.ungroup": "Ungroup",
|
||||
"KanbanCard.copiedLink": "Copied!",
|
||||
"KanbanCard.copyLink": "Copy link",
|
||||
"KanbanCard.delete": "Delete",
|
||||
"KanbanCard.duplicate": "Duplicate",
|
||||
"KanbanCard.untitled": "Untitled",
|
||||
"KanbanCard.copiedLink": "Copied!",
|
||||
"KanbanCard.copyLink": "Copy link",
|
||||
"Mutator.duplicate-board": "duplicate board",
|
||||
"Mutator.new-board-from-template": "new board from template",
|
||||
"Mutator.new-card-from-template": "new card from template",
|
||||
|
@ -88,6 +99,7 @@
|
|||
"PropertyType.URL": "URL",
|
||||
"PropertyType.UpdatedBy": "Last updated by",
|
||||
"PropertyType.UpdatedTime": "Last updated time",
|
||||
"PropertyValueElement.empty": "Empty",
|
||||
"RegistrationLink.confirmRegenerateToken": "This will invalidate previously shared links. Continue?",
|
||||
"RegistrationLink.copiedLink": "Copied!",
|
||||
"RegistrationLink.copyLink": "Copy link",
|
||||
|
@ -146,6 +158,7 @@
|
|||
"View.NewBoardTitle": "Board view",
|
||||
"View.NewGalleryTitle": "Gallery view",
|
||||
"View.NewTableTitle": "Table view",
|
||||
"View.NewTemplateTitle": "Untitled Template",
|
||||
"View.Table": "Table",
|
||||
"ViewHeader.add-template": "New template",
|
||||
"ViewHeader.delete-template": "Delete",
|
||||
|
@ -162,6 +175,7 @@
|
|||
"ViewHeader.search": "Search",
|
||||
"ViewHeader.search-text": "Search text",
|
||||
"ViewHeader.select-a-template": "Select a template",
|
||||
"ViewHeader.set-default-template": "Set as default",
|
||||
"ViewHeader.share-board": "Share board",
|
||||
"ViewHeader.sort": "Sort",
|
||||
"ViewHeader.untitled": "Untitled",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"check": "eslint --ext .tsx,.ts . --quiet --cache && stylelint **/*.scss",
|
||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache && stylelint --fix **/*.scss",
|
||||
"fix:scss": "prettier --write './src/**/*.scss'",
|
||||
"i18n-extract": "formatjs extract src/*/*/*.ts? src/*/*.ts? src/*.ts? --out-file i18n/tmp.json; formatjs compile i18n/tmp.json --out-file i18n/en.json; rm i18n/tmp.json",
|
||||
"i18n-extract": "formatjs extract ../mattermost-plugin/webapp/src/*/*/*.ts? src/*/*/*.ts? src/*/*.ts? src/*.ts? --out-file i18n/tmp.json; formatjs compile i18n/tmp.json --out-file i18n/en.json; rm i18n/tmp.json",
|
||||
"runserver-test": "cd cypress && cross-env FOCALBOARD_SINGLE_USER_TOKEN=TESTTOKEN ../../bin/focalboard-server -single-user",
|
||||
"cypress:ci": "start-server-and-test runserver-test http://localhost:8088 cypress:run",
|
||||
"cypress:run": "cypress run",
|
||||
|
|
|
@ -214,6 +214,15 @@ class OctoClient {
|
|||
return this.getBlocksWithPath(path)
|
||||
}
|
||||
|
||||
async getBlocksWithBlockID(blockID: string, workspaceID?: string, optionalReadToken?: string): Promise<Block[]> {
|
||||
let path = this.workspacePath(workspaceID) + `/blocks?block_id=${blockID}`
|
||||
const readToken = optionalReadToken || Utils.getReadToken()
|
||||
if (readToken) {
|
||||
path += `&read_token=${readToken}`
|
||||
}
|
||||
return this.getBlocksWithPath(path)
|
||||
}
|
||||
|
||||
async getAllBlocks(): Promise<Block[]> {
|
||||
const path = this.workspacePath() + '/blocks?all=true'
|
||||
return this.getBlocksWithPath(path)
|
||||
|
|
Loading…
Reference in a new issue