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:
commit
dee10ca12c
@ -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"
|
||||||
>
|
>
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user