mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-11 18:13:52 +02:00
Config for share board (#1187)
* initial commit for setting shared board configuration * add unit test * working again * update default config * initial commit for setting shared board configuration * add unit test * working again * update default config * add unit tests, some clean up * more cleanup * more clean up * remove header text for GH-1105 * remove unnecessary logs * update text * fix lint errors * more lint fixes * webapp lint fixes * Update mattermost-plugin/plugin.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * Update mattermost-plugin/plugin.json Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com> * update for review, sync with main Co-authored-by: Justine Geffen <justinegeffen@users.noreply.github.com>
This commit is contained in:
parent
fd7be947c1
commit
838c6f90b7
@ -2,7 +2,7 @@
|
||||
"serverRoot": "http://localhost:8000",
|
||||
"port": 8000,
|
||||
"dbtype": "sqlite3",
|
||||
"dbconfig": "./focalboard.db",
|
||||
"dbconfig": "./focalboard.db",
|
||||
"dbtableprefix": "",
|
||||
"postgres_dbconfig": "dbname=focalboard sslmode=disable",
|
||||
"test_dbconfig": "file::memory:?cache=shared",
|
||||
@ -19,6 +19,7 @@
|
||||
"enableLocalMode": true,
|
||||
"localModeSocketLocation": "/var/tmp/focalboard_local.socket",
|
||||
"authMode": "native",
|
||||
"logging_cfg_file": "",
|
||||
"audit_cfg_file": ""
|
||||
"logging_cfg_file": "",
|
||||
"audit_cfg_file": "",
|
||||
"enablepublicsharedboards": false
|
||||
}
|
||||
|
@ -19,8 +19,14 @@
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "For additional setup steps, please [see here](https://focalboard.com/fwlink/plugin-setup.html)",
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": []
|
||||
"settings": [{
|
||||
"key": "EnablePublicSharedBoards",
|
||||
"type": "bool",
|
||||
"display_name": "Enable Publicly-Shared Boards:",
|
||||
"default": false,
|
||||
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -18,6 +19,7 @@ import (
|
||||
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
|
||||
// copy appropriate for your types.
|
||||
type configuration struct {
|
||||
EnablePublicSharedBoards bool
|
||||
}
|
||||
|
||||
// Clone shallow copies the configuration. Your implementation may require a deep copy if
|
||||
@ -70,14 +72,20 @@ func (p *Plugin) setConfiguration(configuration *configuration) {
|
||||
|
||||
// OnConfigurationChange is invoked when configuration changes may have been made.
|
||||
func (p *Plugin) OnConfigurationChange() error {
|
||||
var configuration = new(configuration)
|
||||
// Have we been setup by OnActivate?
|
||||
if p.wsPluginAdapter == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
configuration := &configuration{}
|
||||
// Load the public configuration fields from the Mattermost server configuration.
|
||||
if err := p.API.LoadPluginConfiguration(configuration); err != nil {
|
||||
if err := p.API.LoadPluginConfiguration(&configuration); err != nil {
|
||||
return errors.Wrap(err, "failed to load plugin configuration")
|
||||
}
|
||||
|
||||
p.setConfiguration(configuration)
|
||||
p.server.UpdateClientConfig(utils.StructToMap(configuration))
|
||||
p.wsPluginAdapter.BroadcastConfigChange(*p.server.App().GetClientConfig())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
99
mattermost-plugin/server/configuration_test.go
Normal file
99
mattermost-plugin/server/configuration_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var errLoadPluginConfiguration = errors.New("loadPluginConfiguration Error")
|
||||
|
||||
func TestConfiguration(t *testing.T) {
|
||||
t.Run("null configuration", func(t *testing.T) {
|
||||
plugin := &Plugin{}
|
||||
assert.NotNil(t, plugin.getConfiguration())
|
||||
})
|
||||
|
||||
t.Run("changing configuration", func(t *testing.T) {
|
||||
plugin := &Plugin{}
|
||||
|
||||
configuration1 := &configuration{EnablePublicSharedBoards: false}
|
||||
plugin.setConfiguration(configuration1)
|
||||
assert.Equal(t, configuration1, plugin.getConfiguration())
|
||||
|
||||
configuration2 := &configuration{EnablePublicSharedBoards: true}
|
||||
plugin.setConfiguration(configuration2)
|
||||
assert.Equal(t, configuration2, plugin.getConfiguration())
|
||||
assert.NotEqual(t, configuration1, plugin.getConfiguration())
|
||||
assert.False(t, plugin.getConfiguration() == configuration1)
|
||||
assert.True(t, plugin.getConfiguration() == configuration2)
|
||||
})
|
||||
|
||||
t.Run("setting same configuration", func(t *testing.T) {
|
||||
plugin := &Plugin{}
|
||||
|
||||
configuration1 := &configuration{}
|
||||
plugin.setConfiguration(configuration1)
|
||||
assert.Panics(t, func() {
|
||||
plugin.setConfiguration(configuration1)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("clearing configuration", func(t *testing.T) {
|
||||
plugin := &Plugin{}
|
||||
|
||||
configuration1 := &configuration{EnablePublicSharedBoards: true}
|
||||
plugin.setConfiguration(configuration1)
|
||||
assert.NotPanics(t, func() {
|
||||
plugin.setConfiguration(nil)
|
||||
})
|
||||
assert.NotNil(t, plugin.getConfiguration())
|
||||
assert.NotEqual(t, configuration1, plugin.getConfiguration())
|
||||
})
|
||||
}
|
||||
|
||||
func TestOnConfigurationChange(t *testing.T) {
|
||||
t.Run("Test LoadPlugin Error", func(t *testing.T) {
|
||||
api := &plugintest.API{}
|
||||
api.On("LoadPluginConfiguration",
|
||||
mock.Anything).Return(func(dest interface{}) error {
|
||||
return errLoadPluginConfiguration
|
||||
})
|
||||
|
||||
p := Plugin{}
|
||||
p.SetAPI(api)
|
||||
p.wsPluginAdapter = &ws.PluginAdapter{}
|
||||
|
||||
err := p.OnConfigurationChange()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Test Load Plugin Success", func(t *testing.T) {
|
||||
api := &plugintest.API{}
|
||||
api.On("LoadPluginConfiguration", mock.AnythingOfType("**main.configuration")).Return(nil)
|
||||
|
||||
p := Plugin{}
|
||||
p.SetAPI(api)
|
||||
p.wsPluginAdapter = &FakePluginAdapter{}
|
||||
|
||||
err := p.OnConfigurationChange()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
})
|
||||
}
|
||||
|
||||
var count = 0
|
||||
|
||||
type FakePluginAdapter struct {
|
||||
ws.PluginAdapter
|
||||
}
|
||||
|
||||
func (c *FakePluginAdapter) BroadcastConfigChange(clientConfig model.ClientConfig) {
|
||||
count++
|
||||
}
|
13
mattermost-plugin/server/manifest.go
generated
13
mattermost-plugin/server/manifest.go
generated
@ -34,9 +34,18 @@ const manifestStr = `
|
||||
"bundle_path": "webapp/dist/main.js"
|
||||
},
|
||||
"settings_schema": {
|
||||
"header": "For additional setup steps, please [see here](https://focalboard.com/fwlink/plugin-setup.html)",
|
||||
"header": "",
|
||||
"footer": "",
|
||||
"settings": []
|
||||
"settings": [
|
||||
{
|
||||
"key": "EnablePublicSharedBoards",
|
||||
"display_name": "Enable Publicly-Shared Boards:",
|
||||
"type": "bool",
|
||||
"help_text": "This allows board editors to share boards that can be accessed by anyone with the link.",
|
||||
"placeholder": "",
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
@ -34,7 +34,7 @@ type Plugin struct {
|
||||
configuration *configuration
|
||||
|
||||
server *server.Server
|
||||
wsPluginAdapter *ws.PluginAdapter
|
||||
wsPluginAdapter ws.PluginAdapterInterface
|
||||
}
|
||||
|
||||
func (p *Plugin) OnActivate() error {
|
||||
@ -100,27 +100,33 @@ func (p *Plugin) OnActivate() error {
|
||||
enableTelemetry = *mmconfig.LogSettings.EnableDiagnostics
|
||||
}
|
||||
|
||||
enablePublicSharedBoards := false
|
||||
if mmconfig.PluginSettings.Plugins["focalboard"]["enablepublicsharedboards"] == true {
|
||||
enablePublicSharedBoards = true
|
||||
}
|
||||
|
||||
cfg := &config.Configuration{
|
||||
ServerRoot: baseURL + "/plugins/focalboard",
|
||||
Port: -1,
|
||||
DBType: *mmconfig.SqlSettings.DriverName,
|
||||
DBConfigString: *mmconfig.SqlSettings.DataSource,
|
||||
DBTablePrefix: "focalboard_",
|
||||
UseSSL: false,
|
||||
SecureCookie: true,
|
||||
WebPath: path.Join(*mmconfig.PluginSettings.Directory, "focalboard", "pack"),
|
||||
FilesDriver: *mmconfig.FileSettings.DriverName,
|
||||
FilesPath: *mmconfig.FileSettings.Directory,
|
||||
FilesS3Config: filesS3Config,
|
||||
Telemetry: enableTelemetry,
|
||||
TelemetryID: serverID,
|
||||
WebhookUpdate: []string{},
|
||||
SessionExpireTime: 2592000,
|
||||
SessionRefreshTime: 18000,
|
||||
LocalOnly: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: "",
|
||||
AuthMode: "mattermost",
|
||||
ServerRoot: baseURL + "/plugins/focalboard",
|
||||
Port: -1,
|
||||
DBType: *mmconfig.SqlSettings.DriverName,
|
||||
DBConfigString: *mmconfig.SqlSettings.DataSource,
|
||||
DBTablePrefix: "focalboard_",
|
||||
UseSSL: false,
|
||||
SecureCookie: true,
|
||||
WebPath: path.Join(*mmconfig.PluginSettings.Directory, "focalboard", "pack"),
|
||||
FilesDriver: *mmconfig.FileSettings.DriverName,
|
||||
FilesPath: *mmconfig.FileSettings.Directory,
|
||||
FilesS3Config: filesS3Config,
|
||||
Telemetry: enableTelemetry,
|
||||
TelemetryID: serverID,
|
||||
WebhookUpdate: []string{},
|
||||
SessionExpireTime: 2592000,
|
||||
SessionRefreshTime: 18000,
|
||||
LocalOnly: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: "",
|
||||
AuthMode: "mattermost",
|
||||
EnablePublicSharedBoards: enablePublicSharedBoards,
|
||||
}
|
||||
var db store.Store
|
||||
db, err = sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix, logger, sqlDB, true)
|
||||
|
@ -20,8 +20,6 @@ import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHead
|
||||
import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
|
||||
import {setMattermostTheme} from '../../../webapp/src/theme'
|
||||
|
||||
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK} from './../../../webapp/src/wsclient'
|
||||
|
||||
import TelemetryClient from '../../../webapp/src/telemetry/telemetryClient'
|
||||
|
||||
import '../../../webapp/src/styles/focalboard-variables.scss'
|
||||
@ -29,6 +27,8 @@ import '../../../webapp/src/styles/main.scss'
|
||||
import '../../../webapp/src/styles/labels.scss'
|
||||
import octoClient from '../../../webapp/src/octoClient'
|
||||
|
||||
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG} from './../../../webapp/src/wsclient'
|
||||
|
||||
import manifest from './manifest'
|
||||
import ErrorBoundary from './error_boundary'
|
||||
|
||||
@ -186,6 +186,7 @@ export default class Plugin {
|
||||
|
||||
// register websocket handlers
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data))
|
||||
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data))
|
||||
}
|
||||
|
||||
uninitialize(): void {
|
||||
|
@ -174,7 +174,9 @@ func (a *API) getContainerAllowingReadTokenForBlock(r *http.Request, blockID str
|
||||
}
|
||||
|
||||
// No session, but has valid read token (read-only mode)
|
||||
if len(blockID) > 0 && a.hasValidReadTokenForBlock(r, container, blockID) {
|
||||
if len(blockID) > 0 &&
|
||||
a.hasValidReadTokenForBlock(r, container, blockID) &&
|
||||
a.app.GetClientConfig().EnablePublicSharedBoards {
|
||||
return &container, nil
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,10 @@ type App struct {
|
||||
logger *mlog.Logger
|
||||
}
|
||||
|
||||
func (a *App) SetConfig(config *config.Configuration) {
|
||||
a.config = config
|
||||
}
|
||||
|
||||
func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App {
|
||||
return &App{
|
||||
config: config,
|
||||
|
22
server/app/app_test.go
Normal file
22
server/app/app_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetConfig(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("Test Update Config", func(t *testing.T) {
|
||||
require.False(t, th.App.config.EnablePublicSharedBoards)
|
||||
newConfiguration := config.Configuration{}
|
||||
newConfiguration.EnablePublicSharedBoards = true
|
||||
th.App.SetConfig(&newConfiguration)
|
||||
|
||||
require.True(t, th.App.config.EnablePublicSharedBoards)
|
||||
})
|
||||
}
|
@ -6,7 +6,8 @@ import (
|
||||
|
||||
func (a *App) GetClientConfig() *model.ClientConfig {
|
||||
return &model.ClientConfig{
|
||||
Telemetry: a.config.Telemetry,
|
||||
TelemetryID: a.config.TelemetryID,
|
||||
Telemetry: a.config.Telemetry,
|
||||
TelemetryID: a.config.TelemetryID,
|
||||
EnablePublicSharedBoards: a.config.EnablePublicSharedBoards,
|
||||
}
|
||||
}
|
||||
|
26
server/app/clientConfig_test.go
Normal file
26
server/app/clientConfig_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetClientConfig(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
t.Run("Test Get Client Config", func(t *testing.T) {
|
||||
newConfiguration := config.Configuration{}
|
||||
newConfiguration.Telemetry = true
|
||||
newConfiguration.TelemetryID = "abcde"
|
||||
newConfiguration.EnablePublicSharedBoards = true
|
||||
th.App.SetConfig(&newConfiguration)
|
||||
|
||||
clientConfig := th.App.GetClientConfig()
|
||||
require.True(t, clientConfig.EnablePublicSharedBoards)
|
||||
require.True(t, clientConfig.Telemetry)
|
||||
require.Equal(t, "abcde", clientConfig.TelemetryID)
|
||||
})
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
type ClientConfig struct {
|
||||
Telemetry bool `json:"telemetry"`
|
||||
TelemetryID string `json:"telemetryid"`
|
||||
Telemetry bool `json:"telemetry"`
|
||||
TelemetryID string `json:"telemetryid"`
|
||||
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards"`
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ type Server struct {
|
||||
localRouter *mux.Router
|
||||
localModeServer *http.Server
|
||||
api *api.API
|
||||
app *app.App
|
||||
}
|
||||
|
||||
func New(params Params) (*Server, error) {
|
||||
@ -198,6 +199,7 @@ func New(params Params) (*Server, error) {
|
||||
logger: params.Logger,
|
||||
localRouter: localRouter,
|
||||
api: focalboardAPI,
|
||||
app: app,
|
||||
}
|
||||
|
||||
server.initHandlers()
|
||||
@ -347,6 +349,23 @@ func (s *Server) Logger() *mlog.Logger {
|
||||
return s.logger
|
||||
}
|
||||
|
||||
func (s *Server) App() *app.App {
|
||||
return s.app
|
||||
}
|
||||
|
||||
func (s *Server) UpdateClientConfig(pluginConfig map[string]interface{}) {
|
||||
for index, value := range pluginConfig {
|
||||
if index == "EnablePublicSharedBoards" {
|
||||
b, ok := value.(bool)
|
||||
if !ok {
|
||||
s.logger.Warn("Invalid value for config value", mlog.String(index, value.(string)))
|
||||
}
|
||||
s.config.EnablePublicSharedBoards = b
|
||||
}
|
||||
}
|
||||
s.app.SetConfig(s.config)
|
||||
}
|
||||
|
||||
// Local server
|
||||
|
||||
func (s *Server) startLocalModeServer() error {
|
||||
|
@ -26,27 +26,28 @@ type AmazonS3Config struct {
|
||||
|
||||
// Configuration is the app configuration stored in a json file.
|
||||
type Configuration struct {
|
||||
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
DBType string `json:"dbtype" mapstructure:"dbtype"`
|
||||
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
|
||||
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
|
||||
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
|
||||
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
|
||||
WebPath string `json:"webpath" mapstructure:"webpath"`
|
||||
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
|
||||
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
|
||||
FilesPath string `json:"filespath" mapstructure:"filespath"`
|
||||
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
|
||||
TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"`
|
||||
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
|
||||
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
|
||||
Secret string `json:"secret" mapstructure:"secret"`
|
||||
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
|
||||
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
|
||||
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
|
||||
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
|
||||
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
|
||||
ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"`
|
||||
Port int `json:"port" mapstructure:"port"`
|
||||
DBType string `json:"dbtype" mapstructure:"dbtype"`
|
||||
DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"`
|
||||
DBTablePrefix string `json:"dbtableprefix" mapstructure:"dbtableprefix"`
|
||||
UseSSL bool `json:"useSSL" mapstructure:"useSSL"`
|
||||
SecureCookie bool `json:"secureCookie" mapstructure:"secureCookie"`
|
||||
WebPath string `json:"webpath" mapstructure:"webpath"`
|
||||
FilesDriver string `json:"filesdriver" mapstructure:"filesdriver"`
|
||||
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
|
||||
FilesPath string `json:"filespath" mapstructure:"filespath"`
|
||||
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
|
||||
TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"`
|
||||
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
|
||||
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
|
||||
Secret string `json:"secret" mapstructure:"secret"`
|
||||
SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"`
|
||||
SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"`
|
||||
LocalOnly bool `json:"localonly" mapstructure:"localonly"`
|
||||
EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"`
|
||||
LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"`
|
||||
EnablePublicSharedBoards bool `json:"enablePublicSharedBoards" mapstructure:"enablePublicSharedBoards"`
|
||||
|
||||
AuthMode string `json:"authMode" mapstructure:"authMode"`
|
||||
|
||||
@ -81,6 +82,7 @@ func ReadConfigFile() (*Configuration, error) {
|
||||
viper.SetDefault("LocalOnly", false)
|
||||
viper.SetDefault("EnableLocalMode", false)
|
||||
viper.SetDefault("LocalModeSocketLocation", "/var/tmp/focalboard_local.socket")
|
||||
viper.SetDefault("EnablePublicSharedBoards", false)
|
||||
|
||||
viper.SetDefault("AuthMode", "native")
|
||||
|
||||
|
@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
@ -23,3 +24,9 @@ func CreateGUID() string {
|
||||
func GetMillis() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func StructToMap(v interface{}) (m map[string]interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
_ = json.Unmarshal(b, &m)
|
||||
return
|
||||
}
|
||||
|
@ -11,9 +11,11 @@ const (
|
||||
websocketActionSubscribeBlocks = "SUBSCRIBE_BLOCKS"
|
||||
websocketActionUnsubscribeBlocks = "UNSUBSCRIBE_BLOCKS"
|
||||
websocketActionUpdateBlock = "UPDATE_BLOCK"
|
||||
websocketActionUpdateConfig = "UPDATE_CLIENT_CONFIG"
|
||||
)
|
||||
|
||||
type Adapter interface {
|
||||
BroadcastBlockChange(workspaceID string, block model.Block)
|
||||
BroadcastBlockDelete(workspaceID, blockID, parentID string)
|
||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -9,6 +8,7 @@ import (
|
||||
|
||||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
@ -17,12 +17,6 @@ const websocketMessagePrefix = "custom_focalboard_"
|
||||
|
||||
var errMissingWorkspaceInCommand = fmt.Errorf("command doesn't contain workspaceId")
|
||||
|
||||
func structToMap(v interface{}) (m map[string]interface{}) {
|
||||
b, _ := json.Marshal(v)
|
||||
_ = json.Unmarshal(b, &m)
|
||||
return
|
||||
}
|
||||
|
||||
type PluginAdapterClient struct {
|
||||
webConnID string
|
||||
userID string
|
||||
@ -40,7 +34,6 @@ func (pac *PluginAdapterClient) isSubscribedToWorkspace(workspaceID string) bool
|
||||
return false
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool {
|
||||
for _, id := range pac.blocks {
|
||||
if id == blockID {
|
||||
@ -51,6 +44,24 @@ func (pac *PluginAdapterClient) isSubscribedToBlock(blockID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type PluginAdapterInterface interface {
|
||||
addListener(pac *PluginAdapterClient)
|
||||
removeListener(pac *PluginAdapterClient)
|
||||
removeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string)
|
||||
removeListenerFromBlock(pac *PluginAdapterClient, blockID string)
|
||||
subscribeListenerToWorkspace(pac *PluginAdapterClient, workspaceID string)
|
||||
unsubscribeListenerFromWorkspace(pac *PluginAdapterClient, workspaceID string)
|
||||
unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string)
|
||||
OnWebSocketConnect(webConnID, userID string)
|
||||
OnWebSocketDisconnect(webConnID, userID string)
|
||||
WebSocketMessageHasBeenPosted(webConnID, userID string, req *mmModel.WebSocketRequest)
|
||||
getUserIDsForWorkspace(workspaceID string) []string
|
||||
BroadcastConfigChange(clientConfig model.ClientConfig)
|
||||
BroadcastBlockChange(workspaceID string, block model.Block)
|
||||
BroadcastBlockDelete(workspaceID, blockID, parentID string)
|
||||
HandleClusterEvent(ev mmModel.PluginClusterEvent)
|
||||
}
|
||||
|
||||
type PluginAdapter struct {
|
||||
api plugin.API
|
||||
auth *auth.Auth
|
||||
@ -154,7 +165,6 @@ func (pa *PluginAdapter) unsubscribeListenerFromWorkspace(pac *PluginAdapterClie
|
||||
pa.removeListenerFromWorkspace(pac, workspaceID)
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func (pa *PluginAdapter) unsubscribeListenerFromBlocks(pac *PluginAdapterClient, blockIDs []string) {
|
||||
pa.mu.Lock()
|
||||
defer pa.mu.Unlock()
|
||||
@ -285,6 +295,24 @@ func (pa *PluginAdapter) getUserIDsForWorkspace(workspaceID string) []string {
|
||||
return userIDs
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) sendMessageToAllSkipCluster(payload map[string]interface{}) {
|
||||
// Empty &mmModel.WebsocketBroadcast will send to all users
|
||||
pa.api.PublishWebSocketEvent(websocketActionUpdateConfig, payload, &mmModel.WebsocketBroadcast{})
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) sendMessageToAll(payload map[string]interface{}) {
|
||||
go func() {
|
||||
clusterMessage := &ClusterMessage{Payload: payload}
|
||||
pa.sendMessageToCluster("websocket_message", clusterMessage)
|
||||
}()
|
||||
|
||||
pa.sendMessageToAllSkipCluster(payload)
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastConfigChange(pluginConfig model.ClientConfig) {
|
||||
pa.sendMessageToAll(utils.StructToMap(pluginConfig))
|
||||
}
|
||||
|
||||
// sendWorkspaceMessageSkipCluster sends a message to all the users
|
||||
// with a websocket client connected to.
|
||||
func (pa *PluginAdapter) sendWorkspaceMessageSkipCluster(workspaceID string, payload map[string]interface{}) {
|
||||
@ -320,7 +348,7 @@ func (pa *PluginAdapter) BroadcastBlockChange(workspaceID string, block model.Bl
|
||||
Block: block,
|
||||
}
|
||||
|
||||
pa.sendWorkspaceMessage(workspaceID, structToMap(message))
|
||||
pa.sendWorkspaceMessage(workspaceID, utils.StructToMap(message))
|
||||
}
|
||||
|
||||
func (pa *PluginAdapter) BroadcastBlockDelete(workspaceID, blockID, parentID string) {
|
||||
|
@ -64,6 +64,12 @@ type Server struct {
|
||||
logger *mlog.Logger
|
||||
}
|
||||
|
||||
// UpdateClientConfig is sent on block updates.
|
||||
type UpdateClientConfig struct {
|
||||
Action string `json:"action"`
|
||||
ClientConfig model.ClientConfig `json:"clientconfig"`
|
||||
}
|
||||
|
||||
type websocketSession struct {
|
||||
client *wsClient
|
||||
userID string
|
||||
@ -505,3 +511,27 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastConfigChange broadcasts update messages to clients.
|
||||
func (ws *Server) BroadcastConfigChange(clientConfig model.ClientConfig) {
|
||||
message := UpdateClientConfig{
|
||||
Action: websocketActionUpdateConfig,
|
||||
ClientConfig: clientConfig,
|
||||
}
|
||||
|
||||
listeners := ws.listeners
|
||||
ws.logger.Debug("broadcasting config change to listener(s)",
|
||||
mlog.Int("listener_count", len(listeners)),
|
||||
)
|
||||
|
||||
for listener := range listeners {
|
||||
ws.logger.Debug("Broadcast Config change",
|
||||
mlog.Stringer("remoteAddr", listener.RemoteAddr()),
|
||||
)
|
||||
err := listener.WriteJSON(message)
|
||||
if err != nil {
|
||||
ws.logger.Error("broadcast error", mlog.Err(err))
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import {fetchMe, getLoggedIn, getMe} from './store/users'
|
||||
import {getLanguage, fetchLanguage} from './store/language'
|
||||
import {setGlobalError, getGlobalError} from './store/globalError'
|
||||
import {useAppSelector, useAppDispatch} from './store/hooks'
|
||||
import {fetchClientConfig} from './store/clientConfig'
|
||||
|
||||
import {IUser} from './user'
|
||||
|
||||
@ -79,6 +80,7 @@ const App = React.memo((): JSX.Element => {
|
||||
useEffect(() => {
|
||||
dispatch(fetchLanguage())
|
||||
dispatch(fetchMe())
|
||||
dispatch(fetchClientConfig())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -43,6 +43,7 @@ type Props = {
|
||||
addTemplate: (template: Card) => void
|
||||
shownCardId?: string
|
||||
showCard: (cardId?: string) => void
|
||||
showShared: boolean
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -151,6 +152,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
addCardTemplate={this.addCardTemplate}
|
||||
editCardTemplate={this.editCardTemplate}
|
||||
readonly={this.props.readonly}
|
||||
showShared={this.props.showShared}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,204 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/viewHeader/viewHeaderActionsMenu return menu with Share Boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Export to CSV"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Export to CSV
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Export board archive"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Export board archive
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Share board"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Share board
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/viewHeader/viewHeaderActionsMenu return menu without Share Boards 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ModalWrapper"
|
||||
>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="Button IconButton"
|
||||
role="button"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Menu noselect bottom"
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
>
|
||||
<div
|
||||
class="menu-options"
|
||||
>
|
||||
<div
|
||||
aria-label="Export to CSV"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Export to CSV
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Export board archive"
|
||||
class="MenuOption TextOption menu-option"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Export board archive
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
/>
|
||||
<div
|
||||
class="menu-options hideOnWidescreen"
|
||||
>
|
||||
<div
|
||||
aria-label="Cancel"
|
||||
class="MenuOption TextOption menu-option menu-cancel"
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -37,12 +37,13 @@ type Props = {
|
||||
addCardTemplate: () => void
|
||||
editCardTemplate: (cardTemplateId: string) => void
|
||||
readonly: boolean
|
||||
showShared: boolean
|
||||
}
|
||||
|
||||
const ViewHeader = React.memo((props: Props) => {
|
||||
const [showFilter, setShowFilter] = useState(false)
|
||||
|
||||
const {board, activeView, views, groupByProperty, cards} = props
|
||||
const {board, activeView, views, groupByProperty, cards, showShared} = props
|
||||
|
||||
const withGroupBy = activeView.fields.viewType === 'board' || activeView.fields.viewType === 'table'
|
||||
|
||||
@ -142,6 +143,7 @@ const ViewHeader = React.memo((props: Props) => {
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
showShared={showShared}
|
||||
/>
|
||||
|
||||
{/* New card button */}
|
||||
|
@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactElement} from 'react'
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {IntlProvider} from 'react-intl'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
import ViewHeaderActionsMenu from './viewHeaderActionsMenu'
|
||||
|
||||
const wrapIntl = (children: ReactElement) => (
|
||||
<IntlProvider locale='en'>{children}</IntlProvider>
|
||||
)
|
||||
const board = TestBlockFactory.createBoard()
|
||||
const activeView = TestBlockFactory.createBoardView(board)
|
||||
const card = TestBlockFactory.createCard(board)
|
||||
|
||||
describe('components/viewHeader/viewHeaderActionsMenu', () => {
|
||||
const state = {
|
||||
users: {
|
||||
me: {
|
||||
id: 'user-id-1',
|
||||
username: 'username_1'},
|
||||
},
|
||||
}
|
||||
const mockStore = configureStore([])
|
||||
const store = mockStore(state)
|
||||
|
||||
test('return menu with Share Boards', () => {
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ViewHeaderActionsMenu
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={[card]}
|
||||
showShared={true}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('return menu without Share Boards', () => {
|
||||
const {container} = render(
|
||||
wrapIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<ViewHeaderActionsMenu
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={[card]}
|
||||
showShared={false}
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
),
|
||||
)
|
||||
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||
userEvent.click(buttonElement)
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
})
|
@ -24,6 +24,7 @@ type Props = {
|
||||
board: Board
|
||||
activeView: BoardView
|
||||
cards: Card[]
|
||||
showShared: boolean
|
||||
}
|
||||
|
||||
// import {mutator} from '../../mutator'
|
||||
@ -103,6 +104,8 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
const user = useAppSelector<IUser|null>(getMe)
|
||||
const intl = useIntl()
|
||||
|
||||
const showShareBoard = user && user.id !== 'single-user' && props.showShared
|
||||
|
||||
return (
|
||||
<ModalWrapper>
|
||||
<MenuWrapper>
|
||||
@ -118,7 +121,7 @@ const ViewHeaderActionsMenu = React.memo((props: Props) => {
|
||||
name={intl.formatMessage({id: 'ViewHeader.export-board-archive', defaultMessage: 'Export board archive'})}
|
||||
onClick={() => Archiver.exportBoardArchive(board)}
|
||||
/>
|
||||
{user && user.id !== 'single-user' &&
|
||||
{showShareBoard &&
|
||||
<Menu.Text
|
||||
id='shareBoard'
|
||||
name={intl.formatMessage({id: 'ViewHeader.share-board', defaultMessage: 'Share board'})}
|
||||
|
@ -1,16 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback} from 'react'
|
||||
import React, {useCallback, useEffect} from 'react'
|
||||
import {generatePath, useRouteMatch, useHistory} from 'react-router-dom'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {getCurrentBoard} from '../store/boards'
|
||||
import {getCurrentViewCardsSortedFilteredAndGrouped} from '../store/cards'
|
||||
import {getView, getCurrentBoardViews, getCurrentViewGroupBy, getCurrentView} from '../store/views'
|
||||
import {useAppSelector} from '../store/hooks'
|
||||
import {useAppSelector, useAppDispatch} from '../store/hooks'
|
||||
|
||||
import {getClientConfig, setClientConfig} from '../store/clientConfig'
|
||||
|
||||
import wsClient, {WSClient} from '../wsclient'
|
||||
import {ClientConfig} from '../config/clientConfig'
|
||||
|
||||
import CenterPanel from './centerPanel'
|
||||
import EmptyCenterPanel from './emptyCenterPanel'
|
||||
|
||||
import Sidebar from './sidebar/sidebar'
|
||||
import './workspace.scss'
|
||||
|
||||
@ -25,7 +31,9 @@ function CenterContent(props: Props) {
|
||||
const activeView = useAppSelector(getView(match.params.viewId))
|
||||
const views = useAppSelector(getCurrentBoardViews)
|
||||
const groupByProperty = useAppSelector(getCurrentViewGroupBy)
|
||||
const clientConfig = useAppSelector(getClientConfig)
|
||||
const history = useHistory()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const showCard = useCallback((cardId?: string) => {
|
||||
const params = {...match.params, cardId}
|
||||
@ -33,6 +41,16 @@ function CenterContent(props: Props) {
|
||||
history.push(newPath)
|
||||
}, [match, history])
|
||||
|
||||
useEffect(() => {
|
||||
const onConfigChangeHandler = (_: WSClient, config: ClientConfig) => {
|
||||
dispatch(setClientConfig(config))
|
||||
}
|
||||
wsClient.addOnConfigChange(onConfigChangeHandler)
|
||||
return () => {
|
||||
wsClient.removeOnConfigChange(onConfigChangeHandler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (board && activeView) {
|
||||
let property = groupByProperty
|
||||
if ((!property || property.type !== 'select') && activeView.fields.viewType === 'board') {
|
||||
@ -48,6 +66,7 @@ function CenterContent(props: Props) {
|
||||
activeView={activeView}
|
||||
groupByProperty={property}
|
||||
views={views}
|
||||
showShared={clientConfig?.enablePublicSharedBoards || false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -4,4 +4,5 @@
|
||||
export type ClientConfig = {
|
||||
telemetry: boolean,
|
||||
telemetryid: string,
|
||||
enablePublicSharedBoards: boolean,
|
||||
}
|
||||
|
38
webapp/src/store/clientConfig.ts
Normal file
38
webapp/src/store/clientConfig.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSlice, createAsyncThunk, PayloadAction} from '@reduxjs/toolkit'
|
||||
|
||||
import {ClientConfig} from '../config/clientConfig'
|
||||
|
||||
import {default as client} from '../octoClient'
|
||||
|
||||
import {RootState} from './index'
|
||||
|
||||
export const fetchClientConfig = createAsyncThunk(
|
||||
'clientConfig/fetchClientConfig',
|
||||
async () => client.getClientConfig(),
|
||||
)
|
||||
|
||||
const clientConfigSlice = createSlice({
|
||||
name: 'config',
|
||||
initialState: {value: {telemetry: false, telemetryid: '', enablePublicSharedBoards: false}} as {value: ClientConfig},
|
||||
reducers: {
|
||||
setClientConfig: (state, action: PayloadAction<ClientConfig>) => {
|
||||
state.value = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(fetchClientConfig.fulfilled, (state, action) => {
|
||||
state.value = action.payload || {telemetry: false, telemetryid: '', enablePublicSharedBoards: false}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const {setClientConfig} = clientConfigSlice.actions
|
||||
export const {reducer} = clientConfigSlice
|
||||
|
||||
export function getClientConfig(state: RootState): ClientConfig {
|
||||
return state.clientConfig.value
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {reducer as contentsReducer} from './contents'
|
||||
import {reducer as commentsReducer} from './comments'
|
||||
import {reducer as searchTextReducer} from './searchText'
|
||||
import {reducer as globalErrorReducer} from './globalError'
|
||||
import {reducer as clientConfigReducer} from './clientConfig'
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
@ -28,6 +29,7 @@ const store = configureStore({
|
||||
comments: commentsReducer,
|
||||
searchText: searchTextReducer,
|
||||
globalError: globalErrorReducer,
|
||||
clientConfig: clientConfigReducer,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ClientConfig} from './config/clientConfig'
|
||||
|
||||
import {Utils} from './utils'
|
||||
import {Block} from './blocks/block'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
@ -26,6 +28,7 @@ export const ACTION_SUBSCRIBE_BLOCKS = 'SUBSCRIBE_BLOCKS'
|
||||
export const ACTION_SUBSCRIBE_WORKSPACE = 'SUBSCRIBE_WORKSPACE'
|
||||
export const ACTION_UNSUBSCRIBE_WORKSPACE = 'UNSUBSCRIBE_WORKSPACE'
|
||||
export const ACTION_UNSUBSCRIBE_BLOCKS = 'UNSUBSCRIBE_BLOCKS'
|
||||
export const ACTION_UPDATE_CLIENT_CONFIG = 'UPDATE_CLIENT_CONFIG'
|
||||
|
||||
// The Mattermost websocket client interface
|
||||
export interface MMWebSocketClient {
|
||||
@ -37,6 +40,7 @@ type OnChangeHandler = (client: WSClient, blocks: Block[]) => void
|
||||
type OnReconnectHandler = (client: WSClient) => void
|
||||
type OnStateChangeHandler = (client: WSClient, state: 'init' | 'open' | 'close') => void
|
||||
type OnErrorHandler = (client: WSClient, e: Event) => void
|
||||
type OnConfigChangeHandler = (client: WSClient, clientConfig: ClientConfig) => void
|
||||
|
||||
class WSClient {
|
||||
ws: WebSocket|null = null
|
||||
@ -48,6 +52,7 @@ class WSClient {
|
||||
onReconnect: OnReconnectHandler[] = []
|
||||
onChange: OnChangeHandler[] = []
|
||||
onError: OnErrorHandler[] = []
|
||||
onConfigChange: OnConfigChangeHandler[] = []
|
||||
private mmWSMaxRetries = 100
|
||||
private mmWSRetryDelay = 300
|
||||
private notificationDelay = 100
|
||||
@ -139,6 +144,17 @@ class WSClient {
|
||||
}
|
||||
}
|
||||
|
||||
addOnConfigChange(handler: OnConfigChangeHandler): void {
|
||||
this.onConfigChange.push(handler)
|
||||
}
|
||||
|
||||
removeOnConfigChange(handler: OnConfigChangeHandler): void {
|
||||
const index = this.onConfigChange.indexOf(handler)
|
||||
if (index !== -1) {
|
||||
this.onConfigChange.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
open(): void {
|
||||
if (this.client !== null) {
|
||||
// WSClient needs to ensure that the Mattermost client has
|
||||
@ -208,7 +224,6 @@ class WSClient {
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
// Utils.log(`WSClient websocket onmessage. data: ${e.data}`)
|
||||
if (ws !== this.ws) {
|
||||
Utils.log('Ignoring closed ws')
|
||||
return
|
||||
@ -242,6 +257,12 @@ class WSClient {
|
||||
this.queueUpdateNotification(Utils.fixBlock(message.block!))
|
||||
}
|
||||
|
||||
updateClientConfigHandler(config: ClientConfig): void {
|
||||
for (const handler of this.onConfigChange) {
|
||||
handler(this, config)
|
||||
}
|
||||
}
|
||||
|
||||
authenticate(workspaceId: string, token: string): void {
|
||||
if (!this.hasConn()) {
|
||||
Utils.assertFailure('WSClient.addBlocks: ws is not open')
|
||||
|
Loading…
Reference in New Issue
Block a user