1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-02 14:47:55 +02:00

Merge branch 'main' into GH-1543

This commit is contained in:
Mattermod 2021-10-21 19:00:10 +02:00 committed by GitHub
commit dee10ca12c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 63 deletions

View File

@ -38,14 +38,14 @@ exports[`src/components/gallery/GalleryCard with a comment content should match
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
<div <div
class="Menu noselect left" class="Menu noselect left"
> >
@ -177,14 +177,14 @@ exports[`src/components/gallery/GalleryCard with an image content should match s
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
<div <div
class="Menu noselect left" class="Menu noselect left"
> >
@ -352,14 +352,14 @@ exports[`src/components/gallery/GalleryCard with many contents should match snap
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
<div <div
class="Menu noselect left" class="Menu noselect left"
> >
@ -495,14 +495,14 @@ exports[`src/components/gallery/GalleryCard with many images content should matc
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
<div <div
class="Menu noselect left" class="Menu noselect left"
> >
@ -640,14 +640,14 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
</div> </div>
<div <div
class="gallery-item" class="gallery-item"
@ -680,14 +680,14 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
</div> </div>
<div <div
class="gallery-item" class="gallery-item"
@ -720,14 +720,14 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
</div> </div>
<div <div
class="gallery-item" class="gallery-item"
@ -760,14 +760,14 @@ exports[`src/components/gallery/GalleryCard without block content return Gallery
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
</div> </div>
<div <div
class="gallery-item" class="gallery-item"
@ -800,14 +800,14 @@ exports[`src/components/gallery/GalleryCard without block content should match s
class="MenuWrapper optionsMenu" class="MenuWrapper optionsMenu"
role="button" role="button"
> >
<div <button
class="Button IconButton" class="Button IconButton"
role="button" type="button"
> >
<i <i
class="CompassIcon icon-dots-horizontal OptionsIcon" class="CompassIcon icon-dots-horizontal OptionsIcon"
/> />
</div> </button>
<div <div
class="Menu noselect left" class="Menu noselect left"
> >

View File

@ -47,6 +47,59 @@ test('Basic undo/redo', async () => {
expect(undoManager.redoDescription).toBe(undefined) expect(undoManager.redoDescription).toBe(undefined)
}) })
test('Basic undo/redo response dependant', async () => {
expect(undoManager.canUndo).toBe(false)
expect(undoManager.canRedo).toBe(false)
expect(undoManager.currentCheckpoint).toBe(0)
const blockIds = [2, 1]
const blocks: Record<string, any> = {}
const newBlock = await undoManager.perform(
async () => {
const responseId = blockIds.pop() // every time we run the action a new ID is obtained
const block: Record<string, any> = {id: responseId, title: 'Sample'}
blocks[block.id] = block
return block
},
async (block: Record<string, any>) => {
delete blocks[block.id]
},
'test',
)
// should insert the block and return the new block for its use
expect(undoManager.canUndo).toBe(true)
expect(undoManager.canRedo).toBe(false)
expect(blocks).toHaveProperty('1')
expect(blocks[1]).toEqual(newBlock)
// should correctly remove the block based on the info gathered in
// the redo function
await undoManager.undo()
expect(undoManager.canUndo).toBe(false)
expect(undoManager.canRedo).toBe(true)
expect(blocks).not.toHaveProperty('1')
// when redoing, as the function has side effects the new id will
// be different
await undoManager.redo()
expect(undoManager.canUndo).toBe(true)
expect(undoManager.canRedo).toBe(false)
expect(blocks).not.toHaveProperty('1')
expect(blocks).toHaveProperty('2')
expect(blocks[2].id).toEqual(2)
// when undoing, the undo manager has saved the new id internally
// and it removes the right block
await undoManager.undo()
expect(undoManager.canUndo).toBe(false)
expect(undoManager.canRedo).toBe(true)
expect(blocks).not.toHaveProperty('2')
await undoManager.clear()
})
test('Grouped undo/redo', async () => { test('Grouped undo/redo', async () => {
expect(undoManager.canUndo).toBe(false) expect(undoManager.canUndo).toBe(false)
expect(undoManager.canRedo).toBe(false) expect(undoManager.canRedo).toBe(false)

View File

@ -2,10 +2,11 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
interface UndoCommand { interface UndoCommand {
checkpoint: number checkpoint: number
undo: () => Promise<void> undo: (value?: any) => Promise<void>
redo: () => Promise<void> redo: () => Promise<void>
description?: string description?: string
groupId?: string groupId?: string
value?: any
} }
// //
@ -50,30 +51,36 @@ class UndoManager {
} }
this.isExecuting = true this.isExecuting = true
await command[action]() if (action === 'redo') {
command.value = await command[action]()
} else {
await command[action](command.value)
}
this.isExecuting = false this.isExecuting = false
return this return this
} }
async perform( async perform(
redo: () => Promise<void>, redo: () => Promise<any>,
undo: () => Promise<void>, undo: (value?: any) => Promise<void>,
description?: string, description?: string,
groupId?: string, groupId?: string,
isDiscardable = false, isDiscardable = false,
): Promise<UndoManager> { ): Promise<any> {
await redo() const value = await redo()
return this.registerUndo({undo, redo}, description, groupId, isDiscardable) this.registerUndo({undo, redo}, description, groupId, value, isDiscardable)
return value
} }
registerUndo( registerUndo(
command: { command: {
undo: () => Promise<void>, undo: (value?: any) => Promise<void>,
redo: () => Promise<void> redo: () => Promise<void>
}, },
description?: string, description?: string,
groupId?: string, groupId?: string,
value?: any,
isDiscardable = false, isDiscardable = false,
): UndoManager { ): UndoManager {
if (this.isExecuting) { if (this.isExecuting) {
@ -97,6 +104,7 @@ class UndoManager {
redo: command.redo, redo: command.redo,
description, description,
groupId, groupId,
value,
} }
this.commands.push(internalCommand) this.commands.push(internalCommand)

View File

@ -34,10 +34,10 @@ export const ACTION_UPDATE_CLIENT_CONFIG = 'UPDATE_CLIENT_CONFIG'
export interface MMWebSocketClient { export interface MMWebSocketClient {
conn: WebSocket | null; conn: WebSocket | null;
sendMessage(action: string, data: any, responseCallback?: () => void): void /* eslint-disable-line @typescript-eslint/no-explicit-any */ sendMessage(action: string, data: any, responseCallback?: () => void): void /* eslint-disable-line @typescript-eslint/no-explicit-any */
setFirstConnectCallback(callback: () => void): void
setReconnectCallback(callback: () => void): void setReconnectCallback(callback: () => void): void
setErrorCallback(callback: (event: Event) => void): void setErrorCallback(callback: (event: Event) => void): void
setCloseCallback(callback: (connectFailCount: number) => void): void setCloseCallback(callback: (connectFailCount: number) => void): void
connectionId: string
} }
type OnChangeHandler = (client: WSClient, blocks: Block[]) => void type OnChangeHandler = (client: WSClient, blocks: Block[]) => void
@ -57,8 +57,6 @@ class WSClient {
onChange: OnChangeHandler[] = [] onChange: OnChangeHandler[] = []
onError: OnErrorHandler[] = [] onError: OnErrorHandler[] = []
onConfigChange: OnConfigChangeHandler[] = [] onConfigChange: OnConfigChangeHandler[] = []
private mmWSMaxRetries = 100
private mmWSRetryDelay = 300
private notificationDelay = 100 private notificationDelay = 100
private reopenDelay = 3000 private reopenDelay = 3000
private updatedBlocks: Block[] = [] private updatedBlocks: Block[] = []
@ -163,10 +161,19 @@ class WSClient {
open(): void { open(): void {
if (this.client !== null) { if (this.client !== null) {
// configure the Mattermost websocket client callbacks // configure the Mattermost websocket client callbacks
const onConnect = () => {
Utils.log('WSClient in plugin mode, reusing Mattermost WS connection')
for (const handler of this.onStateChange) {
handler(this, 'open')
}
this.state = 'open'
}
const onReconnect = () => { const onReconnect = () => {
Utils.logWarn('WSClient reconnected') Utils.logWarn('WSClient reconnected')
this.open() onConnect()
for (const handler of this.onReconnect) { for (const handler of this.onReconnect) {
handler(this) handler(this)
} }
@ -204,34 +211,11 @@ class WSClient {
} }
} }
this.client.setFirstConnectCallback(onConnect)
this.client.setErrorCallback(onError) this.client.setErrorCallback(onError)
this.client.setCloseCallback(onClose) this.client.setCloseCallback(onClose)
this.client.setReconnectCallback(onReconnect) this.client.setReconnectCallback(onReconnect)
// WSClient needs to ensure that the Mattermost client has
// correctly stablished the connection before opening
let retries = 0
const setPluginOpen = () => {
if (this.client?.connectionId !== '') {
Utils.log('WSClient in plugin mode, reusing Mattermost WS connection')
for (const handler of this.onStateChange) {
handler(this, 'open')
}
this.state = 'open'
return
}
retries++
if (retries <= this.mmWSMaxRetries) {
Utils.log('WSClient Mattermost Websocket not ready, retrying')
setTimeout(setPluginOpen, this.mmWSRetryDelay)
} else {
Utils.logError('WSClient error on open: Mattermost Websocket client is not ready')
}
}
setPluginOpen()
return return
} }