1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-12-24 13:43:12 +02:00

Global template support

This commit is contained in:
Chen-I Lim 2021-03-30 14:04:00 -07:00
parent d58adf0582
commit 3531c8307d
15 changed files with 196 additions and 72 deletions

View File

@ -140,7 +140,7 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
if a.WorkspaceAuthenticator == nil {
// Native auth: always use root workspace
container := store.Container{
WorkspaceID: "",
WorkspaceID: "0",
}
// Has session
@ -160,11 +160,6 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
vars := mux.Vars(r)
workspaceID := vars["workspaceID"]
if workspaceID == "" || workspaceID == "0" {
// When authenticating, a workspace is required
return nil, errors.New("No workspace specified")
}
container := store.Container{
WorkspaceID: workspaceID,
}

View File

@ -27,7 +27,7 @@ func TestGetParentID(t *testing.T) {
app := New(&cfg, store, auth, wsserver, &mocks.FileBackend{}, webhook)
container := st.Container{
WorkspaceID: "",
WorkspaceID: "0",
}
t.Run("success query", func(t *testing.T) {
store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("test-parent-id", nil)

View File

@ -21,7 +21,7 @@ func (s *SQLStore) latestsBlocksSubquery(c store.Container) sq.SelectBuilder {
FromSelect(internalQuery, "a").
Where(sq.Eq{"rn": 1}).
Where(sq.Eq{"delete_at": 0}).
Where(sq.Eq{"coalesce(workspace_id, '')": c.WorkspaceID})
Where(sq.Eq{"coalesce(workspace_id, '0')": c.WorkspaceID})
}
func (s *SQLStore) GetBlocksWithParentAndType(c store.Container, parentID string, blockType string) ([]model.Block, error) {

View File

@ -34,7 +34,7 @@ func (s *SQLStore) importInitialTemplates() error {
}
globalContainer := store.Container{
WorkspaceID: "",
WorkspaceID: "0",
}
log.Printf("Inserting %d blocks", len(archive.Blocks))

View File

@ -376,12 +376,12 @@ func _000008_teamsDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "000008_teams.down.sql", size: 140, mode: os.FileMode(420), modTime: time.Unix(1616709037, 0)}
info := bindataFileInfo{name: "000008_teams.down.sql", size: 140, mode: os.FileMode(420), modTime: time.Unix(1616780070, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __000008_teamsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x48\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\xe2\x42\xd6\x58\x9c\x91\x58\x94\x99\x97\x4e\x8e\xce\xd4\xe2\xe2\xcc\xfc\x3c\x14\x4b\x13\x4b\x4b\x32\xe2\x8b\x53\x8b\xca\x32\x93\x53\xe1\x5a\x8d\x0c\x34\xad\xb9\x00\x01\x00\x00\xff\xff\xba\x55\x30\xd8\xad\x00\x00\x00")
var __000008_teamsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x48\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\xe2\x42\xd6\x58\x9c\x91\x58\x94\x99\x97\x4e\x8e\xce\xd4\xe2\xe2\xcc\xfc\x3c\x14\x4b\x13\x4b\x4b\x32\xe2\x8b\x53\x8b\xca\x32\x93\x53\xe1\x5a\x8d\x0c\x40\x5a\x43\x03\x5c\x1c\x43\x60\x0e\x55\x08\x76\x0d\x41\xb5\xc7\x56\x41\xdd\x40\x5d\x21\xdc\xc3\x35\xc8\x15\x43\x42\x5d\xc1\x3f\x08\x55\xd0\x33\x58\xc1\x2f\xd4\xc7\xc7\x9a\x0b\x10\x00\x00\xff\xff\x0e\xd0\xa2\xd3\x04\x01\x00\x00")
func _000008_teamsUpSqlBytes() ([]byte, error) {
return bindataRead(
@ -396,7 +396,7 @@ func _000008_teamsUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "000008_teams.up.sql", size: 173, mode: os.FileMode(420), modTime: time.Unix(1616709035, 0)}
info := bindataFileInfo{name: "000008_teams.up.sql", size: 260, mode: os.FileMode(420), modTime: time.Unix(1617137666, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@ -6,3 +6,5 @@ ADD COLUMN workspace_id VARCHAR(36);
ALTER TABLE sessions
ADD COLUMN auth_service VARCHAR(20);
UPDATE blocks SET workspace_id = '0' WHERE workspace_id = '' OR workspace_id IS NULL;

View File

@ -376,12 +376,12 @@ func _000008_teamsDownSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "000008_teams.down.sql", size: 140, mode: os.FileMode(420), modTime: time.Unix(1616709044, 0)}
info := bindataFileInfo{name: "000008_teams.down.sql", size: 140, mode: os.FileMode(420), modTime: time.Unix(1616780070, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __000008_teamsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x48\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\xe2\x42\xd6\x58\x9c\x91\x58\x94\x99\x97\x4e\x8e\xce\xd4\xe2\xe2\xcc\xfc\x3c\x14\x4b\x13\x4b\x4b\x32\xe2\x8b\x53\x8b\xca\x32\x93\x53\xe1\x5a\x8d\x0c\x34\xad\xb9\x00\x01\x00\x00\xff\xff\xba\x55\x30\xd8\xad\x00\x00\x00")
var __000008_teamsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x48\xca\xc9\x4f\xce\x2e\xe6\x72\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x28\xcf\x2f\xca\x2e\x2e\x48\x4c\x4e\x8d\xcf\x4c\x51\x08\x73\x0c\x72\xf6\x70\x0c\xd2\x30\x36\xd3\xb4\xe6\xe2\x42\xd6\x58\x9c\x91\x58\x94\x99\x97\x4e\x8e\xce\xd4\xe2\xe2\xcc\xfc\x3c\x14\x4b\x13\x4b\x4b\x32\xe2\x8b\x53\x8b\xca\x32\x93\x53\xe1\x5a\x8d\x0c\x40\x5a\x43\x03\x5c\x1c\x43\x60\x0e\x55\x08\x76\x0d\x41\xb5\xc7\x56\x41\xdd\x40\x5d\x21\xdc\xc3\x35\xc8\x15\x43\x42\x5d\xc1\x3f\x08\x55\xd0\x33\x58\xc1\x2f\xd4\xc7\xc7\x9a\x0b\x10\x00\x00\xff\xff\x0e\xd0\xa2\xd3\x04\x01\x00\x00")
func _000008_teamsUpSqlBytes() ([]byte, error) {
return bindataRead(
@ -396,7 +396,7 @@ func _000008_teamsUpSql() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "000008_teams.up.sql", size: 173, mode: os.FileMode(420), modTime: time.Unix(1616709052, 0)}
info := bindataFileInfo{name: "000008_teams.up.sql", size: 260, mode: os.FileMode(420), modTime: time.Unix(1617137662, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@ -6,3 +6,5 @@ ADD COLUMN workspace_id VARCHAR(36);
ALTER TABLE sessions
ADD COLUMN auth_service VARCHAR(20);
UPDATE blocks SET workspace_id = '0' WHERE workspace_id = '' OR workspace_id IS NULL;

View File

@ -11,7 +11,7 @@ import (
func StoreTestBlocksStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
container := store.Container{
WorkspaceID: "",
WorkspaceID: "0",
}
t.Run("InsertBlock", func(t *testing.T) {

View File

@ -10,7 +10,7 @@ import (
func StoreTestSharingStore(t *testing.T, setup func(t *testing.T) (store.Store, func())) {
container := store.Container{
WorkspaceID: "",
WorkspaceID: "0",
}
t.Run("UpsertSharingAndGetSharing", func(t *testing.T) {

View File

@ -179,11 +179,6 @@ func (ws *Server) authenticateListener(wsSession *websocketSession, workspaceID,
// Authenticated
// Special case: Default workspace is blank
if workspaceID == "0" {
workspaceID = ""
}
wsSession.workspaceID = workspaceID
wsSession.isAuthenticated = true
log.Printf("authenticateListener: Authenticated, workspaceID: %s", workspaceID)
@ -201,11 +196,6 @@ func (ws *Server) getAuthenticatedWorkspaceID(wsSession *websocketSession, comma
return "", errors.New("No workspace")
}
// Special case: Default workspace is blank
if workspaceID == "0" {
workspaceID = ""
}
container := store.Container{
WorkspaceID: workspaceID,
}

View File

@ -3,9 +3,11 @@
import React from 'react'
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
import {MutableBoard} from '../../blocks/board'
import {Board, MutableBoard} from '../../blocks/board'
import {MutableBoardView} from '../../blocks/boardView'
import mutator from '../../mutator'
import octoClient from '../../octoClient'
import {GlobalTemplateTree, MutableGlobalTemplateTree} from '../../viewModel/globalTemplateTree'
import {WorkspaceTree} from '../../viewModel/workspaceTree'
import IconButton from '../../widgets/buttons/iconButton'
import BoardIcon from '../../widgets/icons/board'
@ -24,13 +26,32 @@ type Props = {
intl: IntlShape
}
class SidebarAddBoardMenu extends React.Component<Props> {
type State = {
globalTemplateTree?: GlobalTemplateTree,
}
class SidebarAddBoardMenu extends React.Component<Props, State> {
state: State = {}
shouldComponentUpdate(): boolean {
return true
}
componentDidMount(): void {
this.syncGlobalTemplates()
}
private async syncGlobalTemplates() {
if (octoClient.workspaceId !== '0' && !this.state.globalTemplateTree) {
const globalTemplateTree = await MutableGlobalTemplateTree.sync()
this.setState({globalTemplateTree})
}
}
render(): JSX.Element {
const {workspaceTree, intl} = this.props
const {globalTemplateTree} = this.state
if (!workspaceTree) {
return <div/>
}
@ -58,44 +79,9 @@ class SidebarAddBoardMenu extends React.Component<Props> {
<Menu.Separator/>
</>}
{workspaceTree.boardTemplates.map((boardTemplate) => {
const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
{workspaceTree.boardTemplates.map((boardTemplate) => this.renderBoardTemplate(boardTemplate))}
return (
<Menu.Text
key={boardTemplate.id}
id={boardTemplate.id}
name={displayName}
icon={<div className='Icon'>{boardTemplate.icon}</div>}
onClick={() => {
this.addBoardFromTemplate(boardTemplate.id)
}}
rightIcon={
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<Menu.Text
icon={<EditIcon/>}
id='edit'
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
this.props.showBoard(boardTemplate.id)
}}
/>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
onClick={async () => {
await mutator.deleteBlock(boardTemplate, 'delete board template')
}}
/>
</Menu>
</MenuWrapper>
}
/>
)
})}
{globalTemplateTree && globalTemplateTree.boardTemplates.map((boardTemplate) => this.renderBoardTemplate(boardTemplate, true))}
<Menu.Text
id='empty-template'
@ -115,6 +101,51 @@ class SidebarAddBoardMenu extends React.Component<Props> {
)
}
private renderBoardTemplate(boardTemplate: Board, isGlobal = false): JSX.Element {
const {intl} = this.props
const displayName = boardTemplate.title || intl.formatMessage({id: 'Sidebar.untitled', defaultMessage: 'Untitled'})
return (
<Menu.Text
key={boardTemplate.id}
id={boardTemplate.id}
name={displayName}
icon={<div className='Icon'>{boardTemplate.icon}</div>}
onClick={() => {
if (isGlobal) {
this.addBoardFromGlobalTemplate(boardTemplate.id)
} else {
this.addBoardFromTemplate(boardTemplate.id)
}
}}
rightIcon={!isGlobal &&
<MenuWrapper stopPropagationOnToggle={true}>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<Menu.Text
icon={<EditIcon/>}
id='edit'
name={intl.formatMessage({id: 'Sidebar.edit-template', defaultMessage: 'Edit'})}
onClick={() => {
this.props.showBoard(boardTemplate.id)
}}
/>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'Sidebar.delete-template', defaultMessage: 'Delete'})}
onClick={async () => {
await mutator.deleteBlock(boardTemplate, 'delete board template')
}}
/>
</Menu>
</MenuWrapper>
}
/>
)
}
private addBoardClicked = async () => {
const {showBoard, intl} = this.props
@ -143,6 +174,24 @@ class SidebarAddBoardMenu extends React.Component<Props> {
)
}
private async addBoardFromGlobalTemplate(boardTemplateId: string) {
const oldBoardId = this.props.activeBoardId
await mutator.duplicateFromRootBoard(
boardTemplateId,
this.props.intl.formatMessage({id: 'Mutator.new-board-from-template', defaultMessage: 'new board from template'}),
false,
async (newBoardId) => {
this.props.showBoard(newBoardId)
},
async () => {
if (oldBoardId) {
this.props.showBoard(oldBoardId)
}
},
)
}
private async addBoardFromTemplate(boardTemplateId: string) {
const oldBoardId = this.props.activeBoardId

View File

@ -6,7 +6,7 @@ import {Board, IPropertyOption, IPropertyTemplate, MutableBoard, PropertyType} f
import {BoardView, ISortOption, MutableBoardView} from './blocks/boardView'
import {Card, MutableCard} from './blocks/card'
import {FilterGroup} from './blocks/filterGroup'
import octoClient from './octoClient'
import octoClient, {OctoClient} from './octoClient'
import {OctoUtils} from './octoUtils'
import undoManager from './undomanager'
import {Utils} from './utils'
@ -579,6 +579,39 @@ class Mutator {
return [newBlocks, newBoard.id]
}
async duplicateFromRootBoard(
boardId: string,
description = 'duplicate board',
asTemplate = false,
afterRedo?: (newBoardId: string) => Promise<void>,
beforeUndo?: () => Promise<void>,
): Promise<[IBlock[], string]> {
const rootClient = new OctoClient(octoClient.serverUrl, '0')
const blocks = await rootClient.getSubtree(boardId, 3)
const [newBlocks1, newBoard] = OctoUtils.duplicateBlockTree(blocks, boardId) as [IBlock[], MutableBoard, Record<string, string>]
const newBlocks = newBlocks1.filter((o) => o.type !== 'comment')
Utils.log(`duplicateBoard: duplicating ${newBlocks.length} blocks`)
if (asTemplate === newBoard.isTemplate) {
newBoard.title = `${newBoard.title} copy`
} else if (asTemplate) {
// Template from board
newBoard.title = 'New board template'
} else {
// Board from template
}
newBoard.isTemplate = asTemplate
await this.insertBlocks(
newBlocks,
description,
async () => {
await afterRedo?.(newBoard.id)
},
beforeUndo,
)
return [newBlocks, newBoard.id]
}
// Other methods
// Not a mutator, but convenient to put here since Mutator wraps OctoClient

View File

@ -24,9 +24,7 @@ class OctoClient {
return readToken
}
workspaceId = '0'
constructor(serverUrl?: string) {
constructor(serverUrl?: string, public workspaceId = '0') {
this.serverUrl = serverUrl || window.location.origin
Utils.log(`OctoClient serverUrl: ${this.serverUrl}`)
}
@ -362,6 +360,7 @@ class OctoClient {
}
}
const client = new OctoClient()
const octoClient = new OctoClient()
export default client
export {OctoClient}
export default octoClient

View File

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IBlock} from '../blocks/block'
import {Board} from '../blocks/board'
import octoClient, {OctoClient} from '../octoClient'
import {OctoUtils} from '../octoUtils'
interface GlobalTemplateTree {
readonly boardTemplates: readonly Board[]
readonly allBlocks: readonly IBlock[]
}
class MutableGlobalTemplateTree implements GlobalTemplateTree {
boardTemplates: Board[] = []
get allBlocks(): IBlock[] {
return [...this.boardTemplates]
}
// Factory methods
static async sync(): Promise<GlobalTemplateTree> {
const rootClient = new OctoClient(octoClient.serverUrl, '0')
const rawBlocks = await rootClient.getBlocksWithType('board')
return this.buildTree(rawBlocks)
}
static incrementalUpdate(originalTree: GlobalTemplateTree, updatedBlocks: IBlock[]): GlobalTemplateTree {
const relevantBlocks = updatedBlocks.filter((block) => block.deleteAt !== 0 || block.type === 'board' || block.type === 'view')
if (relevantBlocks.length < 1) {
// No change
return originalTree
}
const rawBlocks = OctoUtils.mergeBlocks(originalTree.allBlocks, relevantBlocks)
return this.buildTree(rawBlocks)
}
private static buildTree(sourceBlocks: readonly IBlock[]): MutableGlobalTemplateTree {
const blocks = OctoUtils.hydrateBlocks(sourceBlocks)
const workspaceTree = new MutableGlobalTemplateTree()
const allBoards = blocks.filter((block) => block.type === 'board') as Board[]
workspaceTree.boardTemplates = allBoards.filter((block) => block.isTemplate).
sort((a, b) => a.title.localeCompare(b.title)) as Board[]
return workspaceTree
}
// private mutableCopy(): MutableWorkspaceTree {
// return MutableWorkspaceTree.buildTree(this.allBlocks)!
// }
}
export {MutableGlobalTemplateTree, GlobalTemplateTree}