mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-09 08:56:07 +02:00
Merge branch 'main' into private-templates
This commit is contained in:
commit
98ca5b5691
@ -1,8 +1,8 @@
|
||||
.PHONY: run
|
||||
|
||||
run:
|
||||
go run ./main.go
|
||||
go run -tags json1 ./main.go
|
||||
|
||||
build:
|
||||
mkdir -p bin
|
||||
go build -o bin/focalboard-app
|
||||
go build -tags json1 -o bin/focalboard-app
|
||||
|
@ -6,7 +6,7 @@ replace github.com/mattermost/focalboard/server => ../server
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20210422230105-f5ae0b265a8d
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0
|
||||
github.com/webview/webview v0.0.0-20200724072439-e0c01595b361
|
||||
github.com/mattermost/focalboard/server v0.0.0-20220325164658-33557093b00d
|
||||
github.com/mattermost/mattermost-server/v6 v6.5.0
|
||||
github.com/webview/webview v0.0.0-20220314230258-a2b7746141c3
|
||||
)
|
||||
|
1377
linux/go.sum
1377
linux/go.sum
File diff suppressed because it is too large
Load Diff
@ -67,7 +67,6 @@ linters:
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
- gocyclo
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
|
@ -5,8 +5,8 @@ go 1.16
|
||||
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.21
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/mattermost/focalboard/server v0.0.0-20220325164658-33557093b00d
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.27
|
||||
github.com/mattermost/mattermost-server/v6 v6.5.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ import (
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||
apierrors "github.com/mattermost/mattermost-plugin-api/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
@ -113,5 +114,5 @@ func (da *pluginAPIAdapter) GetChannelMember(channelID string, userID string) (*
|
||||
}
|
||||
|
||||
func (da *pluginAPIAdapter) IsErrNotFound(err error) bool {
|
||||
return errors.Is(err, pluginapi.ErrNotFound)
|
||||
return errors.Is(err, apierrors.ErrNotFound)
|
||||
}
|
||||
|
@ -99,6 +99,7 @@ func (p *Plugin) OnActivate() error {
|
||||
NewMutexFn: func(name string) (*cluster.Mutex, error) {
|
||||
return cluster.NewMutex(p.API, name)
|
||||
},
|
||||
PluginAPI: &p.API,
|
||||
}
|
||||
|
||||
var db store.Store
|
||||
|
14912
mattermost-plugin/webapp/package-lock.json
generated
14912
mattermost-plugin/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,71 +14,71 @@
|
||||
"check-types": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.16.8",
|
||||
"@babel/core": "7.16.12",
|
||||
"@babel/plugin-proposal-class-properties": "7.13.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.14.2",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.14.2",
|
||||
"@babel/cli": "7.17.6",
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/plugin-proposal-class-properties": "7.16.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.17.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.16.7",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/polyfill": "7.10.4",
|
||||
"@babel/preset-env": "7.14.2",
|
||||
"@babel/preset-react": "7.13.13",
|
||||
"@babel/preset-typescript": "7.13.0",
|
||||
"@babel/runtime": "7.14.0",
|
||||
"@formatjs/ts-transformer": "3.4.2",
|
||||
"@types/enzyme": "3.10.8",
|
||||
"@types/jest": "26.0.23",
|
||||
"@types/node": "15.6.1",
|
||||
"@types/react": "17.0.6",
|
||||
"@types/react-dom": "17.0.5",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/preset-react": "7.16.7",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@formatjs/ts-transformer": "3.9.2",
|
||||
"@types/enzyme": "3.10.11",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/react-intl": "3.0.0",
|
||||
"@types/react-redux": "7.1.16",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/react-transition-group": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.25.0",
|
||||
"@typescript-eslint/parser": "4.25.0",
|
||||
"@types/react-redux": "7.1.23",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-transition-group": "4.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.16.0",
|
||||
"@typescript-eslint/parser": "5.16.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-jest": "27.0.1",
|
||||
"babel-loader": "8.2.2",
|
||||
"babel-plugin-typescript-to-proptypes": "1.4.2",
|
||||
"css-loader": "5.2.6",
|
||||
"eslint": "7.27.0",
|
||||
"eslint-import-resolver-webpack": "0.13.1",
|
||||
"babel-jest": "27.5.1",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-plugin-typescript-to-proptypes": "2.0.0",
|
||||
"css-loader": "6.7.1",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "2.11.2",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.23.3",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"eslint-plugin-no-only-tests": "2.4.0",
|
||||
"eslint-plugin-react": "7.23.2",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"eslint-plugin-no-only-tests": "2.6.0",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"file-loader": "6.2.0",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"image-webpack-loader": "8.1.0",
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
"imagemin-mozjpeg": "^10.0.0",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"imagemin-pngquant": "^9.0.2",
|
||||
"imagemin-svgo": "^8.0.0",
|
||||
"imagemin-svgo": "^10.0.1",
|
||||
"imagemin-webp": "7.0.0",
|
||||
"jest": "27.0.1",
|
||||
"jest": "27.5.1",
|
||||
"jest-canvas-mock": "2.3.1",
|
||||
"jest-junit": "12.0.0",
|
||||
"sass": "1.25.0",
|
||||
"sass-loader": "11.1.1",
|
||||
"style-loader": "2.0.0",
|
||||
"ts-loader": "9.2.3",
|
||||
"typescript": "4.2.4",
|
||||
"webpack": "5.37.1",
|
||||
"webpack-cli": "4.7.0"
|
||||
"jest-junit": "13.0.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"style-loader": "3.3.1",
|
||||
"ts-loader": "9.2.8",
|
||||
"typescript": "4.6.2",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.12.1",
|
||||
"core-js": "3.21.1",
|
||||
"glob-parent": "6.0.2",
|
||||
"marked": ">=4.0.12",
|
||||
"mattermost-redux": "5.33.1",
|
||||
"react-intl": "^5.13.5",
|
||||
"react-intl": "^5.24.7",
|
||||
"react-router-dom": "5.2.0",
|
||||
"trim-newlines": "4.0.2"
|
||||
},
|
||||
|
@ -112,20 +112,10 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|jpg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'static',
|
||||
publicPath: '/static/',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'static/[name].[ext]',
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -65,4 +65,3 @@ linters:
|
||||
- unconvert
|
||||
- unused
|
||||
- whitespace
|
||||
- gocyclo
|
||||
|
@ -82,7 +82,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH")
|
||||
apiv1.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE")
|
||||
apiv1.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks", a.attachSession(a.handleGetBlocks, false)).Methods("GET")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
||||
@ -289,15 +289,17 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if board.IsTemplate {
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
if !a.hasValidReadTokenForBoard(r, boardID) {
|
||||
if board.IsTemplate {
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3552,7 +3554,6 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("teamID", teamID)
|
||||
|
@ -3,30 +3,40 @@ module github.com/mattermost/focalboard/server
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.0
|
||||
github.com/Masterminds/squirrel v1.5.2
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/mock v1.5.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.15.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
github.com/krolaw/zipstream v0.0.0-20180621105154-0a2661891f94
|
||||
github.com/lib/pq v1.10.2
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.21
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0
|
||||
github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mattermost/mattermost-plugin-api v0.0.27
|
||||
github.com/mattermost/mattermost-server/v6 v6.5.0
|
||||
github.com/mattermost/morph v0.0.0-20220324143723-e4896385ec60
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.23 // indirect
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/rudderlabs/analytics-go v3.3.1+incompatible
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/rudderlabs/analytics-go v3.3.2+incompatible
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/wiggin77/merror v1.0.3
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/yuin/goldmark v1.4.11 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/sqlite v1.15.3 // indirect
|
||||
)
|
||||
|
1375
server/go.sum
1375
server/go.sum
File diff suppressed because it is too large
Load Diff
@ -186,6 +186,21 @@ func (mr *MockAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockAPI)(nil).CreatePost), arg0)
|
||||
}
|
||||
|
||||
// CreateSession mocks base method.
|
||||
func (m *MockAPI) CreateSession(arg0 *model.Session) (*model.Session, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateSession", arg0)
|
||||
ret0, _ := ret[0].(*model.Session)
|
||||
ret1, _ := ret[1].(*model.AppError)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateSession indicates an expected call of CreateSession.
|
||||
func (mr *MockAPIMockRecorder) CreateSession(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockAPI)(nil).CreateSession), arg0)
|
||||
}
|
||||
|
||||
// CreateTeam mocks base method.
|
||||
func (m *MockAPI) CreateTeam(arg0 *model.Team) (*model.Team, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -457,6 +472,20 @@ func (mr *MockAPIMockRecorder) ExecuteSlashCommand(arg0 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSlashCommand", reflect.TypeOf((*MockAPI)(nil).ExecuteSlashCommand), arg0)
|
||||
}
|
||||
|
||||
// ExtendSessionExpiry mocks base method.
|
||||
func (m *MockAPI) ExtendSessionExpiry(arg0 string, arg1 int64) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExtendSessionExpiry", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ExtendSessionExpiry indicates an expected call of ExtendSessionExpiry.
|
||||
func (mr *MockAPIMockRecorder) ExtendSessionExpiry(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendSessionExpiry", reflect.TypeOf((*MockAPI)(nil).ExtendSessionExpiry), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBot mocks base method.
|
||||
func (m *MockAPI) GetBot(arg0 string, arg1 bool) (*model.Bot, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -1573,6 +1602,20 @@ func (mr *MockAPIMockRecorder) InstallPlugin(arg0, arg1 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockAPI)(nil).InstallPlugin), arg0, arg1)
|
||||
}
|
||||
|
||||
// IsEnterpriseReady mocks base method.
|
||||
func (m *MockAPI) IsEnterpriseReady() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsEnterpriseReady")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsEnterpriseReady indicates an expected call of IsEnterpriseReady.
|
||||
func (mr *MockAPIMockRecorder) IsEnterpriseReady() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnterpriseReady", reflect.TypeOf((*MockAPI)(nil).IsEnterpriseReady))
|
||||
}
|
||||
|
||||
// KVCompareAndDelete mocks base method.
|
||||
func (m *MockAPI) KVCompareAndDelete(arg0 string, arg1 []byte) (bool, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2014,6 +2057,20 @@ func (mr *MockAPIMockRecorder) RemoveTeamIcon(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTeamIcon", reflect.TypeOf((*MockAPI)(nil).RemoveTeamIcon), arg0)
|
||||
}
|
||||
|
||||
// RemoveUserCustomStatus mocks base method.
|
||||
func (m *MockAPI) RemoveUserCustomStatus(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveUserCustomStatus", arg0)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveUserCustomStatus indicates an expected call of RemoveUserCustomStatus.
|
||||
func (mr *MockAPIMockRecorder) RemoveUserCustomStatus(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).RemoveUserCustomStatus), arg0)
|
||||
}
|
||||
|
||||
// RequestTrialLicense mocks base method.
|
||||
func (m *MockAPI) RequestTrialLicense(arg0 string, arg1 int, arg2, arg3 bool) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2028,6 +2085,20 @@ func (mr *MockAPIMockRecorder) RequestTrialLicense(arg0, arg1, arg2, arg3 interf
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestTrialLicense", reflect.TypeOf((*MockAPI)(nil).RequestTrialLicense), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// RevokeSession mocks base method.
|
||||
func (m *MockAPI) RevokeSession(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RevokeSession", arg0)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RevokeSession indicates an expected call of RevokeSession.
|
||||
func (mr *MockAPIMockRecorder) RevokeSession(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSession", reflect.TypeOf((*MockAPI)(nil).RevokeSession), arg0)
|
||||
}
|
||||
|
||||
// RevokeUserAccessToken mocks base method.
|
||||
func (m *MockAPI) RevokeUserAccessToken(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2042,6 +2113,20 @@ func (mr *MockAPIMockRecorder) RevokeUserAccessToken(arg0 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockAPI)(nil).RevokeUserAccessToken), arg0)
|
||||
}
|
||||
|
||||
// RolesGrantPermission mocks base method.
|
||||
func (m *MockAPI) RolesGrantPermission(arg0 []string, arg1 string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RolesGrantPermission", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RolesGrantPermission indicates an expected call of RolesGrantPermission.
|
||||
func (mr *MockAPIMockRecorder) RolesGrantPermission(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RolesGrantPermission", reflect.TypeOf((*MockAPI)(nil).RolesGrantPermission), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveConfig mocks base method.
|
||||
func (m *MockAPI) SaveConfig(arg0 *model.Config) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2437,6 +2522,20 @@ func (mr *MockAPIMockRecorder) UpdateUserActive(arg0, arg1 interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserActive", reflect.TypeOf((*MockAPI)(nil).UpdateUserActive), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUserCustomStatus mocks base method.
|
||||
func (m *MockAPI) UpdateUserCustomStatus(arg0 string, arg1 *model.CustomStatus) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserCustomStatus", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateUserCustomStatus indicates an expected call of UpdateUserCustomStatus.
|
||||
func (mr *MockAPIMockRecorder) UpdateUserCustomStatus(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserCustomStatus), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUserStatus mocks base method.
|
||||
func (m *MockAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -213,14 +213,34 @@ func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID stri
|
||||
func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID string) (*model.Board, error) {
|
||||
propertiesBytes, err := json.Marshal(board.Properties)
|
||||
if err != nil {
|
||||
s.logger.Error(
|
||||
"failed to marshal board.Properties",
|
||||
mlog.String("board_id", board.ID),
|
||||
mlog.String("board.Properties", fmt.Sprintf("%v", board.Properties)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cardPropertiesBytes, err := json.Marshal(board.CardProperties)
|
||||
if err != nil {
|
||||
s.logger.Error(
|
||||
"failed to marshal board.CardProperties",
|
||||
mlog.String("board_id", board.ID),
|
||||
mlog.String("board.CardProperties", fmt.Sprintf("%v", board.CardProperties)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columnCalculationsBytes, err := json.Marshal(board.ColumnCalculations)
|
||||
if err != nil {
|
||||
s.logger.Error(
|
||||
"failed to marshal board.ColumnCalculations",
|
||||
mlog.String("board_id", board.ID),
|
||||
mlog.String("board.ColumnCalculations", fmt.Sprintf("%v", board.ColumnCalculations)),
|
||||
mlog.Err(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -290,6 +310,7 @@ func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID stri
|
||||
// writing board history
|
||||
query := insertQuery.SetMap(insertQueryValues).Into(s.tablePrefix + "boards_history")
|
||||
if _, err := query.Exec(); err != nil {
|
||||
s.logger.Error("failed to insert board history", mlog.String("board_id", board.ID), mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -395,7 +416,7 @@ func (s *SQLStore) saveMember(db sq.BaseRunner, bm *model.BoardMember) (*model.B
|
||||
} else {
|
||||
query = query.Suffix(
|
||||
`ON CONFLICT (board_id, user_id)
|
||||
DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor,
|
||||
DO UPDATE SET scheme_admin = EXCLUDED.scheme_admin, scheme_editor = EXCLUDED.scheme_editor,
|
||||
scheme_commenter = EXCLUDED.scheme_commenter, scheme_viewer = EXCLUDED.scheme_viewer`,
|
||||
)
|
||||
}
|
||||
|
@ -5,8 +5,13 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/server/utils"
|
||||
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/mattermost/morph/models"
|
||||
@ -18,7 +23,7 @@ import (
|
||||
mysql "github.com/mattermost/morph/drivers/mysql"
|
||||
postgres "github.com/mattermost/morph/drivers/postgres"
|
||||
sqlite "github.com/mattermost/morph/drivers/sqlite"
|
||||
mbindata "github.com/mattermost/morph/sources/go_bindata"
|
||||
embedded "github.com/mattermost/morph/sources/embedded"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq" // postgres driver
|
||||
@ -33,11 +38,16 @@ import (
|
||||
var assets embed.FS
|
||||
|
||||
const (
|
||||
uniqueIDsMigrationRequiredVersion = 14
|
||||
uniqueIDsMigrationRequiredVersion = 14
|
||||
teamsAndBoardsMigrationRequiredVersion = 17
|
||||
|
||||
teamLessBoardsMigrationKey = "TeamLessBoardsMigrationComplete"
|
||||
|
||||
tempSchemaMigrationTableName = "temp_schema_migration"
|
||||
)
|
||||
|
||||
var errChannelCreatorNotInTeam = errors.New("channel creator not found in user teams")
|
||||
|
||||
func appendMultipleStatementsFlag(connectionString string) (string, error) {
|
||||
config, err := mysqldriver.ParseDSN(connectionString)
|
||||
if err != nil {
|
||||
@ -134,7 +144,7 @@ func (s *SQLStore) Migrate() error {
|
||||
"plugin": s.isPlugin,
|
||||
}
|
||||
|
||||
migrationAssets := &mbindata.AssetSource{
|
||||
migrationAssets := &embedded.AssetSource{
|
||||
Names: assetNamesForDriver,
|
||||
AssetFunc: func(name string) ([]byte, error) {
|
||||
asset, mErr := assets.ReadFile(filepath.Join("migrations", name))
|
||||
@ -157,11 +167,10 @@ func (s *SQLStore) Migrate() error {
|
||||
},
|
||||
}
|
||||
|
||||
src, err := mbindata.WithInstance(migrationAssets)
|
||||
src, err := embedded.WithInstance(migrationAssets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
opts := []morph.EngineOption{
|
||||
morph.WithLock("mm-lock-key"),
|
||||
@ -220,6 +229,23 @@ func (s *SQLStore) Migrate() error {
|
||||
}
|
||||
|
||||
if err := s.deleteOldSchemaMigrationTable(); err != nil {
|
||||
if s.isPlugin {
|
||||
mutex.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureMigrationsAppliedUpToVersion(engine, driver, teamsAndBoardsMigrationRequiredVersion); err != nil {
|
||||
if s.isPlugin {
|
||||
mutex.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.migrateTeamLessBoards(); err != nil {
|
||||
if s.isPlugin {
|
||||
mutex.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -442,6 +468,192 @@ func (s *SQLStore) deleteOldSchemaMigrationTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We no longer support boards existing in DMs and private
|
||||
// group messages. This function migrates all boards
|
||||
// belonging to a DM to the best possible team.
|
||||
func (s *SQLStore) migrateTeamLessBoards() error {
|
||||
if !s.isPlugin {
|
||||
return nil
|
||||
}
|
||||
|
||||
setting, err := s.GetSystemSetting(teamLessBoardsMigrationKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get teamless boards migration state: %w", err)
|
||||
}
|
||||
|
||||
// If the migration is already completed, do not run it again.
|
||||
if hasAlreadyRun, _ := strconv.ParseBool(setting); hasAlreadyRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
boards, err := s.getDMBoards(s.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info(fmt.Sprintf("Migrating %d teamless boards to a team", len(boards)))
|
||||
|
||||
// cache for best suitable team for a DM. Since a DM can
|
||||
// contain multiple boards, caching this avoids
|
||||
// duplicate queries for the same DM.
|
||||
channelToTeamCache := map[string]string{}
|
||||
|
||||
tx, err := s.db.BeginTx(context.Background(), nil)
|
||||
if err != nil {
|
||||
s.logger.Error("error starting transaction in migrateTeamLessBoards", mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range boards {
|
||||
// check the cache first
|
||||
teamID, ok := channelToTeamCache[boards[i].ChannelID]
|
||||
|
||||
// query DB if entry not found in cache
|
||||
if !ok {
|
||||
teamID, err = s.getBestTeamForBoard(s.db, boards[i])
|
||||
if err != nil {
|
||||
// don't let one board's error spoil
|
||||
// the mood for others
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
channelToTeamCache[boards[i].ChannelID] = teamID
|
||||
boards[i].TeamID = teamID
|
||||
|
||||
query := s.getQueryBuilder(tx).
|
||||
Update(s.tablePrefix+"boards").
|
||||
Set("team_id", teamID).
|
||||
Set("type", model.BoardTypePrivate).
|
||||
Where(sq.Eq{"id": boards[i].ID})
|
||||
|
||||
if _, err := query.Exec(); err != nil {
|
||||
s.logger.Error("failed to set team id for board", mlog.String("board_id", boards[i].ID), mlog.String("team_id", teamID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.setSystemSetting(tx, teamLessBoardsMigrationKey, strconv.FormatBool(true)); err != nil {
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
s.logger.Error("transaction rollback error", mlog.Err(rollbackErr), mlog.String("methodName", "migrateTeamLessBoards"))
|
||||
}
|
||||
return fmt.Errorf("cannot mark migration as completed: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
s.logger.Error("failed to commit migrateTeamLessBoards transaction", mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getDMBoards(tx sq.BaseRunner) ([]*model.Board, error) {
|
||||
conditions := sq.And{
|
||||
sq.Eq{"team_id": ""},
|
||||
sq.Or{
|
||||
sq.Eq{"type": "D"},
|
||||
sq.Eq{"type": "G"},
|
||||
},
|
||||
}
|
||||
|
||||
return s.getBoardsByCondition(tx, conditions)
|
||||
}
|
||||
|
||||
// The destination is selected as the first team where all members
|
||||
// of the DM are a part of. If no such team exists,
|
||||
// we use the first team to which DM creator belongs to.
|
||||
func (s *SQLStore) getBestTeamForBoard(tx sq.BaseRunner, board *model.Board) (string, error) {
|
||||
userTeams, err := s.getBoardUserTeams(tx, board)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
teams := [][]interface{}{}
|
||||
for _, userTeam := range userTeams {
|
||||
userTeamInterfaces := make([]interface{}, len(userTeam))
|
||||
for i := range userTeam {
|
||||
userTeamInterfaces[i] = userTeam[i]
|
||||
}
|
||||
teams = append(teams, userTeamInterfaces)
|
||||
}
|
||||
|
||||
commonTeams := utils.Intersection(teams...)
|
||||
var teamID string
|
||||
if len(commonTeams) > 0 {
|
||||
teamID = commonTeams[0].(string)
|
||||
} else {
|
||||
// no common teams found. Let's try finding the best suitable team
|
||||
if board.Type == "D" {
|
||||
// get DM's creator and pick one of their team
|
||||
channel, appErr := (*s.pluginAPI).GetChannel(board.ChannelID)
|
||||
if appErr != nil {
|
||||
s.logger.Error("failed to fetch DM channel for board", mlog.String("board_id", board.ID), mlog.String("channel_id", board.ChannelID), mlog.Err(appErr))
|
||||
return "", appErr
|
||||
}
|
||||
|
||||
if _, ok := userTeams[channel.CreatorId]; !ok {
|
||||
err := fmt.Errorf("%w board_id: %s, channel_id: %s, creator_id: %s", errChannelCreatorNotInTeam, board.ID, board.ChannelID, channel.CreatorId)
|
||||
s.logger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
teamID = userTeams[channel.CreatorId][0]
|
||||
} else if board.Type == "G" {
|
||||
// pick the team that has the most users as members
|
||||
teamFrequency := map[string]int{}
|
||||
highestFrequencyTeam := ""
|
||||
highestFrequencyTeamFrequency := -1
|
||||
|
||||
for _, teams := range userTeams {
|
||||
for _, teamID := range teams {
|
||||
teamFrequency[teamID]++
|
||||
|
||||
if teamFrequency[teamID] > highestFrequencyTeamFrequency {
|
||||
highestFrequencyTeamFrequency = teamFrequency[teamID]
|
||||
highestFrequencyTeam = teamID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
teamID = highestFrequencyTeam
|
||||
}
|
||||
}
|
||||
|
||||
return teamID, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBoardUserTeams(tx sq.BaseRunner, board *model.Board) (map[string][]string, error) {
|
||||
query := s.getQueryBuilder(tx).
|
||||
Select("teammembers.userid", "teammembers.teamid").
|
||||
From("channelmembers").
|
||||
Join("teammembers ON channelmembers.userid = teammembers.userid").
|
||||
Where(sq.Eq{"channelid": board.ChannelID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch user teams for board", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
userTeams := map[string][]string{}
|
||||
|
||||
for rows.Next() {
|
||||
var userID, teamID string
|
||||
err := rows.Scan(&userID, &teamID)
|
||||
if err != nil {
|
||||
s.logger.Error("getBoardUserTeams failed to scan SQL query result", mlog.String("boardID", board.ID), mlog.String("channelID", board.ChannelID), mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userTeams[userID] = append(userTeams[userID], teamID)
|
||||
}
|
||||
|
||||
return userTeams, nil
|
||||
}
|
||||
|
||||
func ensureMigrationsAppliedUpToVersion(engine *morph.Morph, driver drivers.Driver, version int) error {
|
||||
applied, err := driver.AppliedMigrations()
|
||||
if err != nil {
|
||||
|
@ -108,8 +108,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
{{if .postgres}}
|
||||
INSERT INTO {{.prefix}}boards (
|
||||
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, B.title, (B.fields->>'description')::text,
|
||||
B.fields->>'icon', (B.fields->'showDescription')::text::boolean, (B.fields->'isTemplate')::text::boolean,
|
||||
COALESCE((B.fields->'templateVer')::text, '0')::int,
|
||||
B.fields->>'icon',
|
||||
COALESCE((fields->'showDescription')::text::boolean, false),
|
||||
COALESCE((fields->'isTemplate')::text::boolean, false),
|
||||
COALESCE((B.fields->'templateVer')::text::int, 0),
|
||||
'{}', B.fields->'cardProperties', B.fields->'columnCalculations', B.create_at,
|
||||
B.update_at, B.delete_at
|
||||
FROM {{.prefix}}blocks AS B
|
||||
@ -118,8 +120,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
);
|
||||
INSERT INTO {{.prefix}}boards_history (
|
||||
SELECT B.id, B.insert_at, C.TeamId, B.channel_id, B.created_by, B.modified_by, C.type, B.title, (B.fields->>'description')::text,
|
||||
B.fields->>'icon', (B.fields->'showDescription')::text::boolean, (B.fields->'isTemplate')::text::boolean,
|
||||
COALESCE((B.fields->'templateVer')::text, '0')::int,
|
||||
B.fields->>'icon',
|
||||
COALESCE((fields->'showDescription')::text::boolean, false),
|
||||
COALESCE((fields->'isTemplate')::text::boolean, false),
|
||||
COALESCE((B.fields->'templateVer')::text::int, 0),
|
||||
'{}', B.fields->'cardProperties', B.fields->'columnCalculations', B.create_at,
|
||||
B.update_at, B.delete_at
|
||||
FROM {{.prefix}}blocks_history AS B
|
||||
@ -157,8 +161,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
{{if .postgres}}
|
||||
INSERT INTO {{.prefix}}boards (
|
||||
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, (fields->>'description')::text,
|
||||
B.fields->>'icon', (fields->'showDescription')::text::boolean, (fields->'isTemplate')::text::boolean,
|
||||
(B.fields->'templateVer')::text::int,
|
||||
B.fields->>'icon',
|
||||
COALESCE((fields->'showDescription')::text::boolean, false),
|
||||
COALESCE((fields->'isTemplate')::text::boolean, false),
|
||||
COALESCE((B.fields->'templateVer')::text::int, 0),
|
||||
'{}', fields->'cardProperties', fields->'columnCalculations', create_at,
|
||||
update_at, delete_at
|
||||
FROM {{.prefix}}blocks AS B
|
||||
@ -166,8 +172,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
);
|
||||
INSERT INTO {{.prefix}}boards_history (
|
||||
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, (fields->>'description')::text,
|
||||
B.fields->>'icon', (fields->'showDescription')::text::boolean, (fields->'isTemplate')::text::boolean,
|
||||
(B.fields->'templateVer')::text::int,
|
||||
B.fields->>'icon',
|
||||
COALESCE((fields->'showDescription')::text::boolean, false),
|
||||
COALESCE((fields->'isTemplate')::text::boolean, false),
|
||||
COALESCE((B.fields->'templateVer')::text::int, 0),
|
||||
'{}', fields->'cardProperties', fields->'columnCalculations', create_at,
|
||||
update_at, delete_at
|
||||
FROM {{.prefix}}blocks_history AS B
|
||||
@ -177,8 +185,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
{{if .mysql}}
|
||||
INSERT INTO {{.prefix}}boards (
|
||||
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, JSON_UNQUOTE(JSON_EXTRACT(fields,'$.description')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), fields->'$.showDescription', fields->'$.isTemplate',
|
||||
B.fields->'$.templateVer',
|
||||
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')),
|
||||
COALESCE(B.fields->'$.showDescription', 'false') = 'true',
|
||||
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
|
||||
COALESCE(B.fields->'$.templateVer', 0),
|
||||
'{}', fields->'$.cardProperties', fields->'$.columnCalculations', create_at,
|
||||
update_at, delete_at
|
||||
FROM {{.prefix}}blocks AS B
|
||||
@ -186,8 +196,10 @@ CREATE TABLE {{.prefix}}boards_history (
|
||||
);
|
||||
INSERT INTO {{.prefix}}boards_history (
|
||||
SELECT id, insert_at, '0', channel_id, created_by, modified_by, 'O', title, JSON_UNQUOTE(JSON_EXTRACT(fields,'$.description')),
|
||||
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')), fields->'$.showDescription', fields->'$.isTemplate',
|
||||
B.fields->'$.templateVer',
|
||||
JSON_UNQUOTE(JSON_EXTRACT(fields,'$.icon')),
|
||||
COALESCE(B.fields->'$.showDescription', 'false') = 'true',
|
||||
COALESCE(JSON_EXTRACT(B.fields, '$.isTemplate'), 'false') = 'true',
|
||||
COALESCE(B.fields->'$.templateVer', 0),
|
||||
'{}', fields->'$.cardProperties', fields->'$.columnCalculations', create_at,
|
||||
update_at, delete_at
|
||||
FROM {{.prefix}}blocks_history AS B
|
||||
@ -366,7 +378,7 @@ CREATE INDEX idx_boardmembers_user_id ON {{.prefix}}board_members(user_id);
|
||||
INSERT INTO {{.prefix}}board_members (
|
||||
SELECT B.Id, CM.UserId, CM.Roles, (CM.UserId=B.created_by) OR CM.SchemeAdmin, CM.SchemeUser, FALSE, CM.SchemeGuest
|
||||
FROM {{.prefix}}boards AS B
|
||||
INNER JOIN ChannelMembers as CM ON CM.ChannelId=B.channel_id;
|
||||
INNER JOIN ChannelMembers as CM ON CM.ChannelId=B.channel_id
|
||||
);
|
||||
{{else}}
|
||||
{{- /* if we're in personal server or desktop, create memberships for everyone */ -}}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
@ -15,6 +17,7 @@ type Params struct {
|
||||
DB *sql.DB
|
||||
IsPlugin bool
|
||||
NewMutexFn MutexFactory
|
||||
PluginAPI *plugin.API
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
|
@ -3,6 +3,8 @@ package sqlstore
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
@ -20,6 +22,7 @@ type SQLStore struct {
|
||||
isPlugin bool
|
||||
logger *mlog.Logger
|
||||
NewMutexFn MutexFactory
|
||||
pluginAPI *plugin.API
|
||||
}
|
||||
|
||||
// MutexFactory is used by the store in plugin mode to generate
|
||||
@ -42,6 +45,7 @@ func New(params Params) (*SQLStore, error) {
|
||||
logger: params.Logger,
|
||||
isPlugin: params.IsPlugin,
|
||||
NewMutexFn: params.NewMutexFn,
|
||||
pluginAPI: params.PluginAPI,
|
||||
}
|
||||
|
||||
err := store.Migrate()
|
||||
|
@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
@ -54,3 +55,43 @@ func StructToMap(v interface{}) (m map[string]interface{}) {
|
||||
_ = json.Unmarshal(b, &m)
|
||||
return
|
||||
}
|
||||
|
||||
func intersection(a []interface{}, b []interface{}) []interface{} {
|
||||
set := make([]interface{}, 0)
|
||||
hash := make(map[interface{}]bool)
|
||||
av := reflect.ValueOf(a)
|
||||
bv := reflect.ValueOf(b)
|
||||
|
||||
for i := 0; i < av.Len(); i++ {
|
||||
el := av.Index(i).Interface()
|
||||
hash[el] = true
|
||||
}
|
||||
|
||||
for i := 0; i < bv.Len(); i++ {
|
||||
el := bv.Index(i).Interface()
|
||||
if _, found := hash[el]; found {
|
||||
set = append(set, el)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func Intersection(x ...[]interface{}) []interface{} {
|
||||
if len(x) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(x) == 1 {
|
||||
return x[0]
|
||||
}
|
||||
|
||||
result := x[0]
|
||||
i := 1
|
||||
for i < len(x) {
|
||||
result = intersection(result, x[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -186,6 +186,21 @@ func (mr *MockAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockAPI)(nil).CreatePost), arg0)
|
||||
}
|
||||
|
||||
// CreateSession mocks base method.
|
||||
func (m *MockAPI) CreateSession(arg0 *model.Session) (*model.Session, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateSession", arg0)
|
||||
ret0, _ := ret[0].(*model.Session)
|
||||
ret1, _ := ret[1].(*model.AppError)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateSession indicates an expected call of CreateSession.
|
||||
func (mr *MockAPIMockRecorder) CreateSession(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockAPI)(nil).CreateSession), arg0)
|
||||
}
|
||||
|
||||
// CreateTeam mocks base method.
|
||||
func (m *MockAPI) CreateTeam(arg0 *model.Team) (*model.Team, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -457,6 +472,20 @@ func (mr *MockAPIMockRecorder) ExecuteSlashCommand(arg0 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteSlashCommand", reflect.TypeOf((*MockAPI)(nil).ExecuteSlashCommand), arg0)
|
||||
}
|
||||
|
||||
// ExtendSessionExpiry mocks base method.
|
||||
func (m *MockAPI) ExtendSessionExpiry(arg0 string, arg1 int64) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExtendSessionExpiry", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ExtendSessionExpiry indicates an expected call of ExtendSessionExpiry.
|
||||
func (mr *MockAPIMockRecorder) ExtendSessionExpiry(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendSessionExpiry", reflect.TypeOf((*MockAPI)(nil).ExtendSessionExpiry), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBot mocks base method.
|
||||
func (m *MockAPI) GetBot(arg0 string, arg1 bool) (*model.Bot, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -1573,6 +1602,20 @@ func (mr *MockAPIMockRecorder) InstallPlugin(arg0, arg1 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockAPI)(nil).InstallPlugin), arg0, arg1)
|
||||
}
|
||||
|
||||
// IsEnterpriseReady mocks base method.
|
||||
func (m *MockAPI) IsEnterpriseReady() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsEnterpriseReady")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsEnterpriseReady indicates an expected call of IsEnterpriseReady.
|
||||
func (mr *MockAPIMockRecorder) IsEnterpriseReady() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnterpriseReady", reflect.TypeOf((*MockAPI)(nil).IsEnterpriseReady))
|
||||
}
|
||||
|
||||
// KVCompareAndDelete mocks base method.
|
||||
func (m *MockAPI) KVCompareAndDelete(arg0 string, arg1 []byte) (bool, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2014,6 +2057,20 @@ func (mr *MockAPIMockRecorder) RemoveTeamIcon(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTeamIcon", reflect.TypeOf((*MockAPI)(nil).RemoveTeamIcon), arg0)
|
||||
}
|
||||
|
||||
// RemoveUserCustomStatus mocks base method.
|
||||
func (m *MockAPI) RemoveUserCustomStatus(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveUserCustomStatus", arg0)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveUserCustomStatus indicates an expected call of RemoveUserCustomStatus.
|
||||
func (mr *MockAPIMockRecorder) RemoveUserCustomStatus(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).RemoveUserCustomStatus), arg0)
|
||||
}
|
||||
|
||||
// RequestTrialLicense mocks base method.
|
||||
func (m *MockAPI) RequestTrialLicense(arg0 string, arg1 int, arg2, arg3 bool) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2028,6 +2085,20 @@ func (mr *MockAPIMockRecorder) RequestTrialLicense(arg0, arg1, arg2, arg3 interf
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestTrialLicense", reflect.TypeOf((*MockAPI)(nil).RequestTrialLicense), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// RevokeSession mocks base method.
|
||||
func (m *MockAPI) RevokeSession(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RevokeSession", arg0)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RevokeSession indicates an expected call of RevokeSession.
|
||||
func (mr *MockAPIMockRecorder) RevokeSession(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSession", reflect.TypeOf((*MockAPI)(nil).RevokeSession), arg0)
|
||||
}
|
||||
|
||||
// RevokeUserAccessToken mocks base method.
|
||||
func (m *MockAPI) RevokeUserAccessToken(arg0 string) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2042,6 +2113,20 @@ func (mr *MockAPIMockRecorder) RevokeUserAccessToken(arg0 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeUserAccessToken", reflect.TypeOf((*MockAPI)(nil).RevokeUserAccessToken), arg0)
|
||||
}
|
||||
|
||||
// RolesGrantPermission mocks base method.
|
||||
func (m *MockAPI) RolesGrantPermission(arg0 []string, arg1 string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RolesGrantPermission", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RolesGrantPermission indicates an expected call of RolesGrantPermission.
|
||||
func (mr *MockAPIMockRecorder) RolesGrantPermission(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RolesGrantPermission", reflect.TypeOf((*MockAPI)(nil).RolesGrantPermission), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveConfig mocks base method.
|
||||
func (m *MockAPI) SaveConfig(arg0 *model.Config) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
@ -2437,6 +2522,20 @@ func (mr *MockAPIMockRecorder) UpdateUserActive(arg0, arg1 interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserActive", reflect.TypeOf((*MockAPI)(nil).UpdateUserActive), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUserCustomStatus mocks base method.
|
||||
func (m *MockAPI) UpdateUserCustomStatus(arg0 string, arg1 *model.CustomStatus) *model.AppError {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserCustomStatus", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.AppError)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateUserCustomStatus indicates an expected call of UpdateUserCustomStatus.
|
||||
func (mr *MockAPIMockRecorder) UpdateUserCustomStatus(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserCustomStatus", reflect.TypeOf((*MockAPI)(nil).UpdateUserCustomStatus), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUserStatus mocks base method.
|
||||
func (m *MockAPI) UpdateUserStatus(arg0, arg1 string) (*model.Status, *model.AppError) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:mattermost/react",
|
||||
"plugin:react/recommended",
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:jquery/deprecated"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"babel",
|
||||
"mattermost",
|
||||
"import",
|
||||
"cypress",
|
||||
"jquery",
|
||||
@ -25,6 +25,12 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/display-name": [
|
||||
0,
|
||||
{
|
||||
"ignoreTranspilerName": false
|
||||
}
|
||||
],
|
||||
"max-lines": "off",
|
||||
"no-unused-expressions": 0,
|
||||
"babel/no-unused-expressions": [2, {"allowShortCircuit": true}],
|
||||
@ -71,6 +77,7 @@
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"mattermost/no-dispatch-getstate": 0, // Failing in eslint 8
|
||||
"import/no-unresolved": 0, // ts handles this better
|
||||
"camelcase": 0,
|
||||
"semi": "off",
|
||||
|
20343
webapp/package-lock.json
generated
20343
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,29 +23,30 @@
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@draft-js-plugins/editor": "^4.1.0",
|
||||
"@draft-js-plugins/emoji": "^4.5.5",
|
||||
"@draft-js-plugins/mention": "^5.0.0",
|
||||
"@fullcalendar/core": "^5.10.0",
|
||||
"@fullcalendar/daygrid": "^5.10.0",
|
||||
"@fullcalendar/interaction": "^5.10.0",
|
||||
"@fullcalendar/react": "^5.10.0",
|
||||
"@mattermost/compass-icons": "^0.1.10",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@draft-js-plugins/editor": "^4.1.2",
|
||||
"@draft-js-plugins/emoji": "^4.6.0",
|
||||
"@draft-js-plugins/mention": "^5.1.2",
|
||||
"@fullcalendar/core": "^5.10.1",
|
||||
"@fullcalendar/daygrid": "^5.10.1",
|
||||
"@fullcalendar/interaction": "^5.10.1",
|
||||
"@fullcalendar/react": "^5.10.1",
|
||||
"@mattermost/compass-icons": "^0.1.22",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"color": "^4.0.0",
|
||||
"color": "^4.2.1",
|
||||
"draft-js": "^0.11.7",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"fstream": "^1.0.12",
|
||||
"fullcalendar": "^5.10.0",
|
||||
"fullcalendar": "^5.10.2",
|
||||
"imagemin-gifsicle": "^7.0.0",
|
||||
"imagemin-mozjpeg": "^9.0.0",
|
||||
"imagemin-mozjpeg": "^10.0.0",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"imagemin-pngquant": "^9.0.2",
|
||||
"imagemin-svgo": "^8.0.0",
|
||||
"imagemin-svgo": "^10.0.1",
|
||||
"imagemin-webp": "^7.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^4.0.12",
|
||||
"mini-create-react-context": "^0.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"nanoevents": "^5.1.13",
|
||||
"react": "^17.0.2",
|
||||
@ -55,11 +56,11 @@
|
||||
"react-dnd-scrolling": "^1.2.1",
|
||||
"react-dnd-touch-backend": "^14.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-keys": "^2.6.2",
|
||||
"react-hotkeys-hook": "^3.3.0",
|
||||
"react-intl": "^5.13.5",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-hot-keys": "^2.7.1",
|
||||
"react-hotkeys-hook": "^3.4.4",
|
||||
"react-intl": "^5.24.7",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"react-select": "^4.3.0",
|
||||
"trim-newlines": "^4.0.2"
|
||||
},
|
||||
@ -74,8 +75,13 @@
|
||||
}
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
"^.+\\.tsx?$": "@swc/jest"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/nanoevents/"
|
||||
],
|
||||
"maxWorkers": "50%",
|
||||
"testEnvironment": "jsdom",
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{ts,tsx,js,jsx}",
|
||||
@ -83,67 +89,69 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^3.2.0",
|
||||
"@formatjs/ts-transformer": "^3.2.1",
|
||||
"@formatjs/cli": "^4.8.2",
|
||||
"@formatjs/ts-transformer": "^3.9.2",
|
||||
"@swc/jest": "^0.2.20",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"@testing-library/dom": "^7.31.2",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/dom": "^8.11.4",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"@types/color": "^3.0.2",
|
||||
"@types/draft-js": "^0.11.6",
|
||||
"@types/emoji-mart": "^3.0.4",
|
||||
"@types/jest": "^26.0.21",
|
||||
"@types/marked": "^4.0.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/draft-js": "^0.11.9",
|
||||
"@types/emoji-mart": "^3.0.9",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/nanoevents": "^1.0.0",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/react": "^17.0.43",
|
||||
"@types/react-dom": "^17.0.14",
|
||||
"@types/react-intl": "^3.0.0",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-redux": "^7.1.23",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-select": "^4.0.13",
|
||||
"@types/redux-mock-store": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||
"@typescript-eslint/parser": "^4.19.0",
|
||||
"copy-webpack-plugin": "^8.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.16.0",
|
||||
"@typescript-eslint/parser": "^5.16.0",
|
||||
"copy-webpack-plugin": "^10.2.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.0",
|
||||
"cypress": "^9.5.0",
|
||||
"cypress-failed-log": "^2.9.2",
|
||||
"cypress-real-events": "^1.6.0",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-import-resolver-webpack": "0.13.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"cypress": "^9.5.2",
|
||||
"cypress-failed-log": "^2.9.5",
|
||||
"cypress-real-events": "^1.7.0",
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "2.11.2",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"eslint-plugin-no-only-tests": "2.4.0",
|
||||
"eslint-plugin-react": "7.23.1",
|
||||
"eslint-plugin-no-only-tests": "2.6.0",
|
||||
"eslint-plugin-react": "7.29.4",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"image-webpack-loader": "^8.1.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"jest-mock": "^27.5.1",
|
||||
"prettier": "^2.6.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"sass": "^1.32.8",
|
||||
"sass-loader": "^11.0.1",
|
||||
"start-server-and-test": "^1.12.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-sass-guidelines": "^8.0.0",
|
||||
"terser-webpack-plugin": "^5.1.1",
|
||||
"ts-jest": "^26.5.4",
|
||||
"ts-loader": "^8.0.18",
|
||||
"typescript": "^4.2.3",
|
||||
"webpack": "^5.28.0",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
"sass": "^1.49.9",
|
||||
"sass-loader": "^12.6.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-sass-guidelines": "^9.0.1",
|
||||
"terser-webpack-plugin": "^5.3.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"ts-loader": "^9.2.8",
|
||||
"typescript": "^4.6.3",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cypress": "^6.8.0"
|
||||
"cypress": "^9.5.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {createFilterClause} from './blocks/filterClause'
|
||||
|
||||
|
@ -2403,15 +2403,15 @@ exports[`src/components/workspace show add new view tooltip 1`] = `
|
||||
data-escaped=""
|
||||
data-placement="bottom-start"
|
||||
data-reference-hidden=""
|
||||
data-state="hidden"
|
||||
data-state="visible"
|
||||
role="tooltip"
|
||||
style="max-width: 320px; transition-duration: 0ms;"
|
||||
style="max-width: 320px; transition-duration: 250ms;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="tippy-content"
|
||||
data-state="hidden"
|
||||
style="transition-duration: 0ms;"
|
||||
data-state="visible"
|
||||
style="transition-duration: 250ms;"
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
|
@ -6,7 +6,7 @@ import {render, screen, waitFor} from '@testing-library/react'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
|
@ -2,13 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {fireEvent, render, screen} from '@testing-library/react'
|
||||
import {fireEvent, render, screen, act} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import mutator from '../mutator'
|
||||
|
||||
@ -82,19 +82,24 @@ describe('components/blockIconSelector', () => {
|
||||
expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
test('return a new icon after click on EmojiPicker', async () => {
|
||||
const {container} = render(wrapIntl(
|
||||
test('return a new icon after click on EmojiPicker', () => {
|
||||
const {container, getByRole, getAllByRole} = render(wrapIntl(
|
||||
<BlockIconSelector
|
||||
block={card}
|
||||
size='l'
|
||||
/>,
|
||||
))
|
||||
userEvent.click(screen.getByRole('button', {name: 'menuwrapper'}))
|
||||
act(() => {
|
||||
userEvent.click(getByRole('button', {name: 'menuwrapper'}))
|
||||
})
|
||||
const menuPicker = container.querySelector('div#pick')
|
||||
expect(menuPicker).not.toBeNull()
|
||||
fireEvent.mouseEnter(menuPicker!)
|
||||
|
||||
const allButtonThumbUp = await screen.findAllByRole('button', {name: /thumbsup/i})
|
||||
act(() => {
|
||||
fireEvent.mouseEnter(menuPicker!)
|
||||
})
|
||||
|
||||
const allButtonThumbUp = getAllByRole('button', {name: /thumbsup/i})
|
||||
userEvent.click(allButtonThumbUp[0])
|
||||
expect(mockedMutator.changeBlockIcon).toBeCalledTimes(1)
|
||||
expect(mockedMutator.changeBlockIcon).toBeCalledWith(card.boardId, card.id, card.fields.icon, '👍')
|
||||
|
@ -6,7 +6,7 @@ import React from 'react'
|
||||
import {MockStoreEnhanced} from 'redux-mock-store'
|
||||
import {createMemoryHistory} from 'history'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
|
@ -10,7 +10,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {FetchMock} from '../../test/fetchMock'
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
@ -90,6 +90,11 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
@ -226,6 +231,11 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const onboardingBoard = TestBlockFactory.createBoard()
|
||||
@ -326,6 +336,11 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const onboardingBoard = TestBlockFactory.createBoard()
|
||||
@ -424,6 +439,11 @@ describe('components/cardDetail/CardDetail', () => {
|
||||
},
|
||||
current: welcomeCard.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStore(state)
|
||||
|
||||
|
@ -72,6 +72,11 @@ describe('components/cardDetail/cardDetailContents', () => {
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
const store = mockStateStore([], state)
|
||||
const wrap = (child: ReactNode): ReactElement => (
|
||||
|
@ -4,7 +4,7 @@
|
||||
import React from 'react'
|
||||
import {render, screen, act} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import '@testing-library/jest-dom'
|
||||
import {createIntl} from 'react-intl'
|
||||
|
||||
@ -99,6 +99,11 @@ describe('components/cardDetail/CardDetailProperties', () => {
|
||||
},
|
||||
current: card.id,
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockStore = configureStore([])
|
||||
|
@ -6,7 +6,7 @@ import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import moment from 'moment'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
|
@ -64,6 +64,11 @@ describe('components/cardDetail/CommentsList', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const component = (
|
||||
|
@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {fireEvent, render, screen, within} from '@testing-library/react'
|
||||
import {fireEvent, render, screen, within, act} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import React from 'react'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {mockDOM, mockStateStore, wrapDNDIntl} from '../testUtils'
|
||||
@ -277,9 +277,11 @@ describe('components/centerPanel', () => {
|
||||
</ReduxProvider>,
|
||||
))
|
||||
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement).not.toBeNull()
|
||||
userEvent.click(cardElement, {shiftKey: true})
|
||||
act(() => {
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement.parentNode).not.toBeNull()
|
||||
userEvent.click(cardElement as HTMLElement, {shiftKey: true})
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
//escape
|
||||
@ -303,16 +305,20 @@ describe('components/centerPanel', () => {
|
||||
</ReduxProvider>,
|
||||
))
|
||||
|
||||
//select card1
|
||||
const card1Element = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(card1Element).not.toBeNull()
|
||||
userEvent.click(card1Element, {shiftKey: true})
|
||||
act(() => {
|
||||
//select card1
|
||||
const card1Element = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(card1Element).not.toBeNull()
|
||||
userEvent.click(card1Element, {shiftKey: true})
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
//select card2
|
||||
const card2Element = screen.getByRole('textbox', {name: 'card2'})
|
||||
expect(card2Element).not.toBeNull()
|
||||
userEvent.click(card2Element, {shiftKey: true, ctrlKey: true})
|
||||
act(() => {
|
||||
//select card2
|
||||
const card2Element = screen.getByRole('textbox', {name: 'card2'})
|
||||
expect(card2Element).not.toBeNull()
|
||||
userEvent.click(card2Element, {shiftKey: true, ctrlKey: true})
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
//escape
|
||||
@ -335,9 +341,11 @@ describe('components/centerPanel', () => {
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement).not.toBeNull()
|
||||
userEvent.click(cardElement, {shiftKey: true})
|
||||
act(() => {
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement).not.toBeNull()
|
||||
userEvent.click(cardElement, {shiftKey: true})
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
//delete
|
||||
@ -360,9 +368,11 @@ describe('components/centerPanel', () => {
|
||||
/>
|
||||
</ReduxProvider>,
|
||||
))
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement).not.toBeNull()
|
||||
userEvent.click(cardElement, {shiftKey: true})
|
||||
act(() => {
|
||||
const cardElement = screen.getByRole('textbox', {name: 'card1'})
|
||||
expect(cardElement).not.toBeNull()
|
||||
userEvent.click(cardElement, {shiftKey: true})
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
//ctrl+d
|
||||
|
@ -4,7 +4,7 @@
|
||||
import React, {ReactElement, ReactNode} from 'react'
|
||||
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
@ -6,7 +6,7 @@ import {render} from '@testing-library/react'
|
||||
|
||||
import {act} from 'react-dom/test-utils'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {ImageBlock} from '../../blocks/imageBlock'
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {TextBlock} from '../../blocks/textBlock'
|
||||
|
||||
|
@ -5,7 +5,7 @@ import '@testing-library/jest-dom'
|
||||
import {act, render, screen} from '@testing-library/react'
|
||||
|
||||
import React, {ReactNode, ReactElement} from 'react'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapDNDIntl, mockStateStore, blocksById} from '../../testUtils'
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {MockStoreEnhanced} from 'redux-mock-store'
|
||||
|
||||
|
@ -5,12 +5,12 @@ import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createMemoryHistory} from 'history'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
import {render, act} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
@ -77,8 +77,12 @@ describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
userEvent.hover(container.querySelector('#lang') as Element)
|
||||
act(() => {
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
})
|
||||
act(() => {
|
||||
userEvent.hover(container.querySelector('#lang') as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@ -91,8 +95,12 @@ describe('components/sidebar/GlobalHeaderSettingsMenu', () => {
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
userEvent.hover(container.querySelector('#import') as Element)
|
||||
act(() => {
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
})
|
||||
act(() => {
|
||||
userEvent.hover(container.querySelector('#import') as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
userEvent.click(container.querySelector('[aria-label="Asana"]') as Element)
|
||||
|
@ -373,6 +373,7 @@ exports[`components/kanban/calculations/KanbanCalculationOptions with submenu op
|
||||
</span>
|
||||
<div
|
||||
class="dropdown-submenu"
|
||||
style="top: -10px; left: 0px;"
|
||||
>
|
||||
<div
|
||||
class="drops "
|
||||
|
@ -6,7 +6,7 @@ import '@testing-library/jest-dom'
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {MemoryRouter} from 'react-router-dom'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {IPropertyOption, IPropertyTemplate} from '../../blocks/board'
|
||||
|
@ -9,7 +9,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import Mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
|
@ -4,7 +4,7 @@ import React from 'react'
|
||||
import {fireEvent, render, screen, within} from '@testing-library/react'
|
||||
import {createIntl} from 'react-intl'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import Mutator from '../../mutator'
|
||||
|
@ -5,7 +5,7 @@ import {render, screen, within} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {createIntl} from 'react-intl'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapDNDIntl} from '../../testUtils'
|
||||
import Mutator from '../../mutator'
|
||||
|
@ -38,6 +38,11 @@ describe('components/onboardingTour/addComments/AddCommentTourStep', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -38,6 +38,11 @@ describe('components/onboardingTour/addComments/AddDescriptionTourStep', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -38,6 +38,11 @@ describe('components/onboardingTour/addComments/AddPropertiesTourStep', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -32,6 +32,11 @@ describe('components/onboardingTour/addComments/AddViewTourStep', () => {
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -32,6 +32,11 @@ describe('components/onboardingTour/addComments/CopyLinkTourStep', () => {
|
||||
},
|
||||
current: 'board_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -38,6 +38,11 @@ describe('components/onboardingTour/addComments/OpenCardTourStep', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -38,6 +38,11 @@ describe('components/onboardingTour/addComments/ShareBoardTourStep', () => {
|
||||
},
|
||||
current: 'card_id_1',
|
||||
},
|
||||
clientConfig: {
|
||||
value: {
|
||||
featureFlags: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
let store = mockStore(state)
|
||||
|
||||
|
@ -11,6 +11,8 @@ import {OnboardingBoardTitle, OnboardingCardTitle} from '../../cardDetail/cardDe
|
||||
import {getOnboardingTourCategory, getOnboardingTourStarted, getOnboardingTourStep} from '../../../store/users'
|
||||
import TourTip from '../../tutorial_tour_tip/tutorial_tour_tip'
|
||||
import {TutorialTourTipPunchout} from '../../tutorial_tour_tip/tutorial_tour_tip_backdrop'
|
||||
import {ClientConfig} from '../../../config/clientConfig'
|
||||
import {getClientConfig} from '../../../store/clientConfig'
|
||||
|
||||
type Props = {
|
||||
requireCard: boolean
|
||||
@ -30,12 +32,13 @@ type Props = {
|
||||
|
||||
const TourTipRenderer = (props: Props): JSX.Element | null => {
|
||||
const board = useAppSelector(getCurrentBoard)
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
|
||||
const isOnboardingBoard = board ? board.title === OnboardingBoardTitle : false
|
||||
const onboardingTourStarted = useAppSelector(getOnboardingTourStarted)
|
||||
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
|
||||
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
|
||||
const showTour = isOnboardingBoard && onboardingTourStarted && onboardingTourCategory === props.category
|
||||
const showTour = !clientConfig.featureFlags.disableTour && isOnboardingBoard && onboardingTourStarted && onboardingTourCategory === props.category
|
||||
let showTourTip = showTour && onboardingTourStep === props.step.toString()
|
||||
|
||||
if (props.requireCard) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
import React, {useState} from 'react'
|
||||
import {render, screen} from '@testing-library/react'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
@ -7,7 +7,7 @@ import thunk from 'redux-thunk'
|
||||
|
||||
import React from 'react'
|
||||
import {MemoryRouter} from 'react-router'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {IUser} from '../../user'
|
||||
import {ISharing} from '../../blocks/sharing'
|
||||
|
@ -4,12 +4,12 @@
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
import {render, act} from '@testing-library/react'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
@ -73,8 +73,12 @@ describe('components/sidebar/SidebarSettingsMenu', () => {
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
userEvent.hover(container.querySelector('#theme') as Element)
|
||||
act(() => {
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
})
|
||||
act(() => {
|
||||
userEvent.hover(container.querySelector('#theme') as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@ -86,8 +90,12 @@ describe('components/sidebar/SidebarSettingsMenu', () => {
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
userEvent.hover(container.querySelector('#lang') as Element)
|
||||
act(() => {
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
})
|
||||
act(() => {
|
||||
userEvent.hover(container.querySelector('#lang') as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@ -100,8 +108,12 @@ describe('components/sidebar/SidebarSettingsMenu', () => {
|
||||
)
|
||||
|
||||
const {container} = render(component)
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
userEvent.hover(container.querySelector('#import') as Element)
|
||||
act(() => {
|
||||
userEvent.click(container.querySelector('.menu-entry') as Element)
|
||||
})
|
||||
act(() => {
|
||||
userEvent.hover(container.querySelector('#import') as Element)
|
||||
})
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
userEvent.click(container.querySelector('[aria-label="Asana"]') as Element)
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {render} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapDNDIntl} from '../testUtils'
|
||||
import {Constants} from '../constants'
|
||||
|
@ -7,7 +7,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
|
@ -5,7 +5,7 @@ import React from 'react'
|
||||
import {render, screen} from '@testing-library/react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {FilterClause} from '../../blocks/filterClause'
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
|
@ -8,7 +8,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {MockStoreEnhanced} from 'redux-mock-store'
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {BoardView} from '../../blocks/boardView'
|
||||
|
||||
|
@ -7,7 +7,7 @@ import {Provider as ReduxProvider} from 'react-redux'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import {wrapIntl, mockStateStore} from '../../testUtils'
|
||||
|
||||
|
@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'
|
||||
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
|
@ -4,7 +4,7 @@ import {act, render, waitFor} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {MemoryRouter} from 'react-router-dom'
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
@ -449,15 +449,15 @@ describe('src/components/workspace', () => {
|
||||
<Workspace readonly={false}/>
|
||||
</ReduxProvider>,
|
||||
), {wrapper: MemoryRouter})
|
||||
jest.runOnlyPendingTimers()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
const elements = document.querySelectorAll('.AddViewTourStep')
|
||||
expect(elements).toBeDefined()
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
jest.runOnlyPendingTimers()
|
||||
|
||||
await waitFor(() => expect(document.querySelectorAll('.AddViewTourStep')).toBeDefined(), {timeout: 5000})
|
||||
|
||||
const elements = document.querySelectorAll('.AddViewTourStep')
|
||||
expect(elements.length).toBe(2)
|
||||
expect(elements[1]).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('show copy link tooltip', async () => {
|
||||
|
@ -261,7 +261,11 @@ class OctoClient {
|
||||
}
|
||||
|
||||
async getAllBlocks(boardID: string): Promise<Block[]> {
|
||||
const path = `/api/v1/boards/${boardID}/blocks?all=true`
|
||||
let path = `/api/v1/boards/${boardID}/blocks?all=true`
|
||||
const readToken = Utils.getReadToken()
|
||||
if (readToken) {
|
||||
path += `&read_token=${readToken}`
|
||||
}
|
||||
return this.getBlocksWithPath(path)
|
||||
}
|
||||
|
||||
@ -619,7 +623,11 @@ class OctoClient {
|
||||
}
|
||||
|
||||
async getBoard(boardID: string): Promise<Board | undefined> {
|
||||
const path = `/api/v1/boards/${boardID}`
|
||||
let path = `/api/v1/boards/${boardID}`
|
||||
const readToken = Utils.getReadToken()
|
||||
if (readToken) {
|
||||
path += `?read_token=${readToken}`
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {
|
||||
method: 'GET',
|
||||
headers: this.headers(),
|
||||
|
@ -80,13 +80,10 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
return initialLoad
|
||||
}, [props.readonly])
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (userId: string, boardTeamId: string, boardId: string, viewId: string) => {
|
||||
// set the active board if we're able to pick one
|
||||
dispatch(setCurrentBoard(boardId))
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (userId: string, boardTeamId: string, boardId: string) => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0) {
|
||||
if (result.payload.blocks.length === 0 && userId) {
|
||||
const member = await octoClient.createBoardMember({userId, boardId})
|
||||
if (!member) {
|
||||
UserSettings.setLastBoardID(boardTeamId, null)
|
||||
@ -101,23 +98,28 @@ const BoardPage = (props: Props): JSX.Element => {
|
||||
teamId: boardTeamId,
|
||||
boardId,
|
||||
}))
|
||||
|
||||
// and set it as most recently viewed board
|
||||
UserSettings.setLastBoardID(boardTeamId, boardId)
|
||||
|
||||
if (viewId && viewId !== '0') {
|
||||
dispatch(setCurrentView(viewId))
|
||||
UserSettings.setLastViewId(boardId, viewId)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(loadAction(match.params.boardId))
|
||||
|
||||
if (match.params.boardId && me) {
|
||||
loadOrJoinBoard(me.id, teamId, match.params.boardId, match.params.viewId)
|
||||
if (match.params.boardId) {
|
||||
// set the active board
|
||||
dispatch(setCurrentBoard(match.params.boardId))
|
||||
|
||||
// and set it as most recently viewed board
|
||||
UserSettings.setLastBoardID(teamId, match.params.boardId)
|
||||
|
||||
if (match.params.viewId && match.params.viewId !== '0') {
|
||||
dispatch(setCurrentView(match.params.viewId))
|
||||
UserSettings.setLastViewId(match.params.boardId, match.params.viewId)
|
||||
}
|
||||
|
||||
if (!props.readonly && me) {
|
||||
loadOrJoinBoard(me.id, teamId, match.params.boardId)
|
||||
}
|
||||
}
|
||||
}, [teamId, match.params.boardId, match.params.viewId])
|
||||
}, [teamId, match.params.boardId, match.params.viewId, me?.id])
|
||||
|
||||
if (props.readonly) {
|
||||
useEffect(() => {
|
||||
|
@ -14,7 +14,7 @@ import userEvent from '@testing-library/user-event'
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
|
||||
import {mocked} from 'ts-jest/utils'
|
||||
import {mocked} from 'jest-mock'
|
||||
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
|
@ -4,7 +4,6 @@ import React from 'react'
|
||||
import {
|
||||
Redirect,
|
||||
Route,
|
||||
useRouteMatch,
|
||||
} from 'react-router-dom'
|
||||
|
||||
import {Utils} from './utils'
|
||||
@ -12,6 +11,8 @@ import {getLoggedIn, getMe} from './store/users'
|
||||
import {useAppSelector} from './store/hooks'
|
||||
import {UserSettingKey} from './userSettings'
|
||||
import {IUser, UserPropPrefix} from './user'
|
||||
import {getClientConfig} from './store/clientConfig'
|
||||
import {ClientConfig} from './config/clientConfig'
|
||||
|
||||
type RouteProps = {
|
||||
path: string|string[]
|
||||
@ -25,46 +26,51 @@ type RouteProps = {
|
||||
|
||||
function FBRoute(props: RouteProps) {
|
||||
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||
const match = useRouteMatch<any>()
|
||||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const clientConfig = useAppSelector<ClientConfig>(getClientConfig)
|
||||
|
||||
let originalPath
|
||||
if (props.getOriginalPath) {
|
||||
originalPath = props.getOriginalPath(match)
|
||||
}
|
||||
let redirect: React.ReactNode = null
|
||||
|
||||
if (Utils.isFocalboardPlugin() && (me?.id !== 'single-user') && props.path !== '/welcome' && loggedIn === true && !me?.props[UserPropPrefix + UserSettingKey.WelcomePageViewed]) {
|
||||
if (originalPath) {
|
||||
return <Redirect to={`/welcome?r=${originalPath}`}/>
|
||||
}
|
||||
return <Redirect to='/welcome'/>
|
||||
}
|
||||
const showWelcomePage = !clientConfig.featureFlags.disableTour &&
|
||||
Utils.isFocalboardPlugin() &&
|
||||
(me?.id !== 'single-user') &&
|
||||
props.path !== '/welcome' &&
|
||||
loggedIn === true &&
|
||||
!me?.props[UserPropPrefix + UserSettingKey.WelcomePageViewed]
|
||||
|
||||
if (loggedIn === false && props.loginRequired) {
|
||||
if (originalPath) {
|
||||
let redirectUrl = '/' + Utils.buildURL(originalPath)
|
||||
if (redirectUrl.indexOf('//') === 0) {
|
||||
redirectUrl = redirectUrl.slice(1)
|
||||
if (showWelcomePage) {
|
||||
redirect = ({match}: any) => {
|
||||
if (props.getOriginalPath) {
|
||||
return <Redirect to={`/welcome?r=${props.getOriginalPath!(match)}`}/>
|
||||
}
|
||||
const loginUrl = `/error?id=not-logged-in&r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
return <Redirect to='/welcome'/>
|
||||
}
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
|
||||
if (loggedIn === true || !props.loginRequired) {
|
||||
return (
|
||||
<Route
|
||||
path={props.path}
|
||||
render={props.render}
|
||||
component={props.component}
|
||||
exact={props.exact}
|
||||
>
|
||||
{props.children}
|
||||
</Route>
|
||||
)
|
||||
if (redirect === null && loggedIn === false && props.loginRequired) {
|
||||
redirect = ({match}: any) => {
|
||||
if (props.getOriginalPath) {
|
||||
let redirectUrl = '/' + Utils.buildURL(props.getOriginalPath!(match))
|
||||
if (redirectUrl.indexOf('//') === 0) {
|
||||
redirectUrl = redirectUrl.slice(1)
|
||||
}
|
||||
const loginUrl = `/error?id=not-logged-in&r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return <Redirect to='/error?id=not-logged-in'/>
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
return (
|
||||
<Route
|
||||
path={props.path}
|
||||
render={props.render}
|
||||
component={props.component}
|
||||
exact={props.exact}
|
||||
>
|
||||
{redirect || props.children}
|
||||
</Route>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FBRoute)
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
useRouteMatch,
|
||||
useHistory,
|
||||
generatePath,
|
||||
useLocation,
|
||||
} from 'react-router-dom'
|
||||
import {createBrowserHistory, History} from 'history'
|
||||
|
||||
@ -70,16 +71,21 @@ function HomeToCurrentTeam(props: {path: string, exact: boolean}) {
|
||||
|
||||
function WorkspaceToTeamRedirect() {
|
||||
const match = useRouteMatch<{boardId: string, viewId: string, cardId?: string, workspaceId?: string}>()
|
||||
const queryParams = new URLSearchParams(useLocation().search)
|
||||
const history = useHistory()
|
||||
useEffect(() => {
|
||||
octoClient.getBoard(match.params.boardId).then((board) => {
|
||||
if (board) {
|
||||
history.replace(generatePath('/team/:teamId/:boardId?/:viewId?/:cardId?', {
|
||||
let newPath = generatePath(match.path.replace('/workspace/:workspaceId', '/team/:teamId'), {
|
||||
teamId: board?.teamId,
|
||||
boardId: board?.id,
|
||||
viewId: match.params.viewId,
|
||||
cardId: match.params.cardId,
|
||||
}))
|
||||
})
|
||||
if (queryParams) {
|
||||
newPath += '?' + queryParams
|
||||
}
|
||||
history.replace(newPath)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
@ -159,9 +165,10 @@ const FocalboardRouter = (props: Props): JSX.Element => {
|
||||
<ChangePasswordPage/>
|
||||
</FBRoute>}
|
||||
|
||||
<FBRoute path='/shared/:boardId?/:viewId?/:cardId?'>
|
||||
<FBRoute path={['/team/:teamId/shared/:boardId?/:viewId?/:cardId?', '/shared/:boardId?/:viewId?/:cardId?']}>
|
||||
<BoardPage readonly={true}/>
|
||||
</FBRoute>
|
||||
|
||||
<FBRoute
|
||||
loginRequired={true}
|
||||
path='/board/:boardId?/:viewId?/:cardId?'
|
||||
@ -171,7 +178,7 @@ const FocalboardRouter = (props: Props): JSX.Element => {
|
||||
>
|
||||
<BoardPage/>
|
||||
</FBRoute>
|
||||
<FBRoute path={['/workspace/:workspaceId/:boardId?/:viewId?/:cardId?', '/workspace/:workspaceId/shared/:boardId?/:viewId?/:cardId?']}>
|
||||
<FBRoute path={['/workspace/:workspaceId/shared/:boardId?/:viewId?/:cardId?', '/workspace/:workspaceId/:boardId?/:viewId?/:cardId?']}>
|
||||
<WorkspaceToTeamRedirect/>
|
||||
</FBRoute>
|
||||
<FBRoute
|
||||
|
@ -7,7 +7,7 @@ import {default as client} from '../octoClient'
|
||||
import {Board, BoardMember} from '../blocks/board'
|
||||
import {IUser} from '../user'
|
||||
|
||||
import {initialLoad, loadBoardData} from './initialLoad'
|
||||
import {initialLoad, initialReadOnlyLoad, loadBoardData} from './initialLoad'
|
||||
|
||||
import {addBoardUsers} from './users'
|
||||
|
||||
@ -147,6 +147,17 @@ const boardsSlice = createSlice({
|
||||
builder.addCase(loadBoardData.rejected, (state) => {
|
||||
state.loadingBoard = false
|
||||
})
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.boards = {}
|
||||
state.templates = {}
|
||||
if (action.payload.board) {
|
||||
if (action.payload.board.isTemplate) {
|
||||
state.templates[action.payload.board.id] = action.payload.board
|
||||
} else {
|
||||
state.boards[action.payload.board.id] = action.payload.board
|
||||
}
|
||||
}
|
||||
})
|
||||
builder.addCase(initialLoad.fulfilled, (state, action) => {
|
||||
state.boards = {}
|
||||
action.payload.boards.forEach((board) => {
|
||||
|
@ -59,7 +59,7 @@ const cardsSlice = createSlice({
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.cards = {}
|
||||
state.templates = {}
|
||||
for (const block of action.payload) {
|
||||
for (const block of action.payload.blocks) {
|
||||
if (block.type === 'card' && block.fields.isTemplate) {
|
||||
state.templates[block.id] = block as Card
|
||||
} else if (block.type === 'card' && !block.fields.isTemplate) {
|
||||
@ -109,7 +109,7 @@ export function getCard(cardId: string): (state: RootState) => Card|undefined {
|
||||
}
|
||||
|
||||
export const getCurrentBoardCards = createSelector(
|
||||
(state) => state.boards.current,
|
||||
(state: RootState) => state.boards.current,
|
||||
getCards,
|
||||
(boardId, cards) => {
|
||||
return Object.values(cards).filter((c) => c.boardId === boardId) as Card[]
|
||||
@ -117,7 +117,7 @@ export const getCurrentBoardCards = createSelector(
|
||||
)
|
||||
|
||||
export const getCurrentBoardTemplates = createSelector(
|
||||
(state) => state.boards.current,
|
||||
(state: RootState) => state.boards.current,
|
||||
getTemplates,
|
||||
(boardId, templates) => {
|
||||
return Object.values(templates).filter((c) => c.boardId === boardId) as Card[]
|
||||
|
@ -53,7 +53,7 @@ const commentsSlice = createSlice({
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.comments = {}
|
||||
state.commentsByCard = {}
|
||||
for (const block of action.payload) {
|
||||
for (const block of action.payload.blocks) {
|
||||
if (block.type === 'comment') {
|
||||
state.comments[block.id] = block as CommentBlock
|
||||
state.commentsByCard[block.parentId] = state.commentsByCard[block.parentId] || []
|
||||
|
@ -54,7 +54,7 @@ const contentsSlice = createSlice({
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.contents = {}
|
||||
state.contentsByCard = {}
|
||||
for (const block of action.payload) {
|
||||
for (const block of action.payload.blocks) {
|
||||
if (block.type !== 'board' && block.type !== 'view' && block.type !== 'comment') {
|
||||
state.contents[block.id] = block as ContentBlock
|
||||
state.contentsByCard[block.parentId] = state.contentsByCard[block.parentId] || []
|
||||
@ -90,8 +90,8 @@ export const getContents = createSelector(
|
||||
|
||||
export function getCardContents(cardId: string): (state: RootState) => Array<ContentBlock|ContentBlock[]> {
|
||||
return createSelector(
|
||||
(state) => (state.contents?.contentsByCard && state.contents.contentsByCard[cardId]) || [],
|
||||
(state) => getCards(state)[cardId]?.fields?.contentOrder || getTemplates(state)[cardId]?.fields?.contentOrder,
|
||||
(state: RootState) => (state.contents?.contentsByCard && state.contents.contentsByCard[cardId]) || [],
|
||||
(state: RootState) => getCards(state)[cardId]?.fields?.contentOrder || getTemplates(state)[cardId]?.fields?.contentOrder,
|
||||
(contents, contentOrder): Array<ContentBlock|ContentBlock[]> => {
|
||||
const result: Array<ContentBlock|ContentBlock[]> = []
|
||||
if (!contents) {
|
||||
|
@ -37,8 +37,12 @@ export const initialLoad = createAsyncThunk(
|
||||
export const initialReadOnlyLoad = createAsyncThunk(
|
||||
'initialReadOnlyLoad',
|
||||
async (boardId: string) => {
|
||||
const blocks = client.getSubtree(boardId, 3)
|
||||
return blocks
|
||||
const [board, blocks] = await Promise.all([
|
||||
client.getBoard(boardId),
|
||||
client.getAllBlocks(boardId),
|
||||
])
|
||||
|
||||
return {board, blocks}
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -79,7 +79,7 @@ const viewsSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(initialReadOnlyLoad.fulfilled, (state, action) => {
|
||||
state.views = {}
|
||||
for (const block of action.payload) {
|
||||
for (const block of action.payload.blocks) {
|
||||
if (block.type === 'view') {
|
||||
state.views[block.id] = block as BoardView
|
||||
}
|
||||
@ -129,7 +129,7 @@ export function getView(viewId: string): (state: RootState) => BoardView|null {
|
||||
}
|
||||
|
||||
export const getCurrentBoardViews = createSelector(
|
||||
(state) => state.boards.current,
|
||||
(state: RootState) => state.boards.current,
|
||||
getViews,
|
||||
(boardId, views) => {
|
||||
Utils.log(`getCurrentBoardViews boardId: ${boardId} views: ${views.length}`)
|
||||
|
@ -68,19 +68,10 @@ function makeCommonConfig() {
|
||||
},
|
||||
{
|
||||
test: /\.(eot|tiff|svg|woff2|woff|ttf|png|jpg|jpeg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'static',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'image-webpack-loader',
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'static/[name].[ext]',
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user