mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-29 21:01:01 +02:00
Merge branch 'main' into close-template-selector
This commit is contained in:
commit
9e2e91e522
4
Makefile
4
Makefile
@ -100,12 +100,12 @@ server-linux-package-docker:
|
|||||||
rm -rf package
|
rm -rf package
|
||||||
|
|
||||||
generate: ## Install and run code generators.
|
generate: ## Install and run code generators.
|
||||||
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
|
cd server; go get github.com/golang/mock/mockgen
|
||||||
cd server; go generate ./...
|
cd server; go generate ./...
|
||||||
|
|
||||||
server-lint: templates-archive ## Run linters on server code.
|
server-lint: templates-archive ## Run linters on server code.
|
||||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
|
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
cd server; golangci-lint run ./...
|
cd server; golangci-lint run ./...
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
run:
|
run:
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
modules-download-mode: readonly
|
modules-download-mode: readonly
|
||||||
|
skip-files:
|
||||||
|
- product/boards_product.go
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gofmt:
|
gofmt:
|
||||||
|
@ -75,7 +75,7 @@ endif
|
|||||||
|
|
||||||
ifneq ($(HAS_SERVER),)
|
ifneq ($(HAS_SERVER),)
|
||||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
|
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
|
|
||||||
|
@ -214,5 +214,23 @@ func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
|||||||
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Preferences service.
|
||||||
|
//
|
||||||
|
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||||
|
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
||||||
|
return p, normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||||
|
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
|
||||||
|
return normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||||
|
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
|
||||||
|
return normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the adapter implements ServicesAPI.
|
// Ensure the adapter implements ServicesAPI.
|
||||||
var _ model.ServicesAPI = &serviceAPIAdapter{}
|
var _ model.ServicesAPI = &serviceAPIAdapter{}
|
||||||
|
@ -44,6 +44,7 @@ func init() {
|
|||||||
app.KVStoreKey: {},
|
app.KVStoreKey: {},
|
||||||
app.StoreKey: {},
|
app.StoreKey: {},
|
||||||
app.SystemKey: {},
|
app.SystemKey: {},
|
||||||
|
app.PreferencesKey: {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -66,11 +67,11 @@ type boardsProduct struct {
|
|||||||
kvStoreService product.KVStoreService
|
kvStoreService product.KVStoreService
|
||||||
storeService product.StoreService
|
storeService product.StoreService
|
||||||
systemService product.SystemService
|
systemService product.SystemService
|
||||||
|
preferencesService product.PreferencesService
|
||||||
|
|
||||||
boardsApp *boards.BoardsApp
|
boardsApp *boards.BoardsApp
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo,exhaustive
|
|
||||||
func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (app.Product, error) {
|
func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (app.Product, error) {
|
||||||
boards := &boardsProduct{}
|
boards := &boardsProduct{}
|
||||||
|
|
||||||
@ -178,6 +179,12 @@ func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (a
|
|||||||
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||||
}
|
}
|
||||||
boards.systemService = systemService
|
boards.systemService = systemService
|
||||||
|
case app.PreferencesKey:
|
||||||
|
preferencesService, ok := service.(product.PreferencesService)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||||
|
}
|
||||||
|
boards.preferencesService = preferencesService
|
||||||
case app.HooksKey: // not needed
|
case app.HooksKey: // not needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -216,5 +216,23 @@ func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
|||||||
// NOOP for plugin
|
// NOOP for plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Preferences service.
|
||||||
|
//
|
||||||
|
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||||
|
p, appErr := a.api.GetPreferencesForUser(userID)
|
||||||
|
return p, normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *pluginAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||||
|
appErr := a.api.UpdatePreferencesForUser(userID, preferences)
|
||||||
|
return normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *pluginAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||||
|
appErr := a.api.DeletePreferencesForUser(userID, preferences)
|
||||||
|
return normalizeAppErr(appErr)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the adapter implements ServicesAPI.
|
// Ensure the adapter implements ServicesAPI.
|
||||||
var _ model.ServicesAPI = &pluginAPIAdapter{}
|
var _ model.ServicesAPI = &pluginAPIAdapter{}
|
||||||
|
@ -23,7 +23,7 @@ exports[`components/boardSelector renders with no results 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -126,7 +126,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -327,7 +327,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -23,15 +23,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
height: 450px;
|
height: 450px;
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 18px;
|
|
||||||
top: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-dialog-box {
|
.confirmation-dialog-box {
|
||||||
.dialog {
|
.dialog {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -165,6 +165,13 @@ func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
|
|||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *API) userIsGuest(userID string) (bool, error) {
|
||||||
|
if a.singleUserToken != "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return a.app.UserIsGuest(userID)
|
||||||
|
}
|
||||||
|
|
||||||
// Response helpers
|
// Response helpers
|
||||||
|
|
||||||
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
|
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
|
||||||
|
@ -134,6 +134,16 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
file, handle, err := r.FormFile(UploadFormFileKey)
|
file, handle, err := r.FormFile(UploadFormFileKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(w, "%v", err)
|
fmt.Fprintf(w, "%v", err)
|
||||||
@ -206,7 +216,13 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
auditRec.AddMeta("TeamID", teamID)
|
auditRec.AddMeta("TeamID", teamID)
|
||||||
|
|
||||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
|
@ -381,17 +381,6 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
|
|||||||
UpdateAt: now,
|
UpdateAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.app.GetUser(userID)
|
|
||||||
if err != nil {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.IsGuest {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "guests not supported", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), sessionContextKey, session)
|
ctx := context.WithValue(r.Context(), sessionContextKey, session)
|
||||||
handler(w, r.WithContext(ctx))
|
handler(w, r.WithContext(ctx))
|
||||||
return
|
return
|
||||||
|
@ -104,6 +104,19 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if board.IsTemplate {
|
||||||
|
var isGuest bool
|
||||||
|
isGuest, err = a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guest are not allowed to get board templates"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
|
||||||
@ -221,13 +234,6 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
val := r.URL.Query().Get("disable_notify")
|
val := r.URL.Query().Get("disable_notify")
|
||||||
disableNotify := val == True
|
disableNotify := val == True
|
||||||
|
|
||||||
// in phase 1 we use "manage_board_cards", but we would have to
|
|
||||||
// check on specific actions for phase 2
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
requestBody, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -242,6 +248,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasComments := false
|
||||||
|
hasContents := false
|
||||||
for _, block := range blocks {
|
for _, block := range blocks {
|
||||||
// Error checking
|
// Error checking
|
||||||
if len(block.Type) < 1 {
|
if len(block.Type) < 1 {
|
||||||
@ -250,6 +258,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if block.Type == model.TypeComment {
|
||||||
|
hasComments = true
|
||||||
|
} else {
|
||||||
|
hasContents = true
|
||||||
|
}
|
||||||
|
|
||||||
if block.CreateAt < 1 {
|
if block.CreateAt < 1 {
|
||||||
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||||
@ -269,6 +283,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasContents {
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasComments {
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
||||||
@ -748,9 +775,16 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
if block.Type == model.TypeComment {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||||
return
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
|
||||||
|
@ -65,8 +65,14 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
auditRec.AddMeta("teamID", teamID)
|
auditRec.AddMeta("teamID", teamID)
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// retrieve boards list
|
// retrieve boards list
|
||||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
|
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
@ -143,6 +149,16 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = newBoard.IsValid(); err != nil {
|
if err = newBoard.IsValid(); err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
||||||
return
|
return
|
||||||
@ -233,6 +249,19 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
var isGuest bool
|
||||||
|
isGuest, err = a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
@ -502,6 +531,16 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
auditRec.AddMeta("boardID", boardID)
|
auditRec.AddMeta("boardID", boardID)
|
||||||
|
@ -98,6 +98,16 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, block := range newBab.Blocks {
|
for _, block := range newBab.Blocks {
|
||||||
// Error checking
|
// Error checking
|
||||||
if len(block.Type) < 1 {
|
if len(block.Type) < 1 {
|
||||||
|
@ -246,6 +246,16 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guests not allowed to join boards"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
newBoardMember := &model.BoardMember{
|
newBoardMember := &model.BoardMember{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
BoardID: boardID,
|
BoardID: boardID,
|
||||||
@ -421,6 +431,16 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
SchemeViewer: reqBoardMember.SchemeViewer,
|
SchemeViewer: reqBoardMember.SchemeViewer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(paramsUserID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isGuest {
|
||||||
|
newBoardMember.SchemeAdmin = false
|
||||||
|
}
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||||
return
|
return
|
||||||
|
@ -53,6 +53,16 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
|
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
@ -146,8 +146,14 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
auditRec.AddMeta("teamID", teamID)
|
auditRec.AddMeta("teamID", teamID)
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// retrieve boards list
|
// retrieve boards list
|
||||||
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
|
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
@ -299,8 +305,14 @@ func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// retrieve boards list
|
// retrieve boards list
|
||||||
boards, err := a.app.SearchBoardsForUser(term, userID)
|
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
return
|
return
|
||||||
|
@ -226,7 +226,17 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
|
|
||||||
users, err := a.app.SearchTeamUsers(teamID, searchQuery)
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
asGuestUser := ""
|
||||||
|
if isGuest {
|
||||||
|
asGuestUser = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err)
|
||||||
return
|
return
|
||||||
|
@ -51,6 +51,16 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGuest, err := a.userIsGuest(userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to templates"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
auditRec.AddMeta("teamID", teamID)
|
auditRec.AddMeta("teamID", teamID)
|
||||||
|
@ -219,6 +219,19 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||||
|
|
||||||
|
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !canSeeUser {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userData, err := json.Marshal(user)
|
userData, err := json.Marshal(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
@ -256,8 +256,8 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
|||||||
return bab, members, err
|
return bab, members, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
|
func (a *App) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
return a.store.GetBoardsForUserAndTeam(userID, teamID)
|
return a.store.GetBoardsForUserAndTeam(userID, teamID, includePublicBoards)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
|
func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
|
||||||
@ -552,8 +552,8 @@ func (a *App) DeleteBoardMember(boardID, userID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SearchBoardsForUser(term, userID string) ([]*model.Board, error) {
|
func (a *App) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
return a.store.SearchBoardsForUser(term, userID)
|
return a.store.SearchBoardsForUser(term, userID, includePublicBoards)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {
|
func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {
|
||||||
|
@ -65,7 +65,7 @@ func (a *App) GetUserTimezone(userID string) (string, error) {
|
|||||||
|
|
||||||
func getUserBoards(userID string, teamID string, a *App) ([]string, error) {
|
func getUserBoards(userID string, teamID string, a *App) ([]string, error) {
|
||||||
// get boards accessible by user and filter boardIDs
|
// get boards accessible by user and filter boardIDs
|
||||||
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID)
|
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("error getting boards for user")
|
return nil, errors.New("error getting boards for user")
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
|
|||||||
IsGuest: false,
|
IsGuest: false,
|
||||||
}
|
}
|
||||||
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
||||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
|
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
|
||||||
th.Store.EXPECT().
|
th.Store.EXPECT().
|
||||||
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
||||||
Return(mockTeamInsightsList, nil)
|
Return(mockTeamInsightsList, nil)
|
||||||
@ -72,7 +72,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
|
|||||||
IsGuest: false,
|
IsGuest: false,
|
||||||
}
|
}
|
||||||
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
||||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
|
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
|
||||||
th.Store.EXPECT().
|
th.Store.EXPECT().
|
||||||
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
||||||
Return(nil, insightError{"board-insight-error"})
|
Return(nil, insightError{"board-insight-error"})
|
||||||
|
@ -5,12 +5,12 @@ import (
|
|||||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) {
|
func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, error) {
|
||||||
return a.store.GetUsersByTeam(teamID)
|
return a.store.GetUsersByTeam(teamID, asGuestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) {
|
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||||
return a.store.SearchUsersByTeam(teamID, searchQuery)
|
return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
|
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
|
||||||
@ -26,6 +26,29 @@ func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[st
|
|||||||
return user.Props, nil
|
return user.Props, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) UserIsGuest(userID string) (bool, error) {
|
||||||
|
user, err := a.store.GetUserByID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return user.IsGuest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) {
|
||||||
|
isGuest, err := a.UserIsGuest(seerUser)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if isGuest {
|
||||||
|
hasSharedChannels, err := a.store.CanSeeUser(seerUser, seenUser)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return hasSharedChannels, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) {
|
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) {
|
||||||
return a.store.SearchUserChannels(teamID, userID, query)
|
return a.store.SearchUserChannels(teamID, userID, query)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
|
//go:generate mockgen -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -263,7 +263,7 @@ func TestCreateBoard(t *testing.T) {
|
|||||||
th.CheckBadRequest(resp)
|
th.CheckBadRequest(resp)
|
||||||
require.Nil(t, board)
|
require.Nil(t, board)
|
||||||
|
|
||||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, boards)
|
require.Empty(t, boards)
|
||||||
})
|
})
|
||||||
@ -277,7 +277,7 @@ func TestCreateBoard(t *testing.T) {
|
|||||||
th.CheckBadRequest(resp)
|
th.CheckBadRequest(resp)
|
||||||
require.Nil(t, board)
|
require.Nil(t, board)
|
||||||
|
|
||||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, boards)
|
require.Empty(t, boards)
|
||||||
})
|
})
|
||||||
@ -292,7 +292,7 @@ func TestCreateBoard(t *testing.T) {
|
|||||||
th.CheckForbidden(resp)
|
th.CheckForbidden(resp)
|
||||||
require.Nil(t, board)
|
require.Nil(t, board)
|
||||||
|
|
||||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, boards)
|
require.Empty(t, boards)
|
||||||
})
|
})
|
||||||
@ -530,7 +530,7 @@ func TestSearchBoards(t *testing.T) {
|
|||||||
Type: model.BoardTypePrivate,
|
Type: model.BoardTypePrivate,
|
||||||
TeamID: "other-team-id",
|
TeamID: "other-team-id",
|
||||||
}
|
}
|
||||||
_, err = th.Server.App().CreateBoard(board5, user1.ID, true)
|
rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@ -543,13 +543,13 @@ func TestSearchBoards(t *testing.T) {
|
|||||||
Name: "should return all boards where user1 is member or that are public",
|
Name: "should return all boards where user1 is member or that are public",
|
||||||
Client: th.Client,
|
Client: th.Client,
|
||||||
Term: "board",
|
Term: "board",
|
||||||
ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID},
|
ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID, rBoard5.ID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "matching a full word",
|
Name: "matching a full word",
|
||||||
Client: th.Client,
|
Client: th.Client,
|
||||||
Term: "admin",
|
Term: "admin",
|
||||||
ExpectedIDs: []string{rBoard1.ID, rBoard3.ID},
|
ExpectedIDs: []string{rBoard1.ID, rBoard3.ID, rBoard5.ID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "matching part of the word",
|
Name: "matching part of the word",
|
||||||
@ -1595,6 +1595,52 @@ func TestUpdateMember(t *testing.T) {
|
|||||||
require.Len(t, members, 1)
|
require.Len(t, members, 1)
|
||||||
require.True(t, members[0].SchemeAdmin)
|
require.True(t, members[0].SchemeAdmin)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should always disable the admin role on update member if the user is a guest", func(t *testing.T) {
|
||||||
|
th := SetupTestHelperPluginMode(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
clients := setupClients(th)
|
||||||
|
|
||||||
|
newBoard := &model.Board{
|
||||||
|
Title: "title",
|
||||||
|
Type: model.BoardTypeOpen,
|
||||||
|
TeamID: teamID,
|
||||||
|
}
|
||||||
|
board, err := th.Server.App().CreateBoard(newBoard, userAdmin, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newGuestMember := &model.BoardMember{
|
||||||
|
UserID: userGuest,
|
||||||
|
BoardID: board.ID,
|
||||||
|
SchemeViewer: true,
|
||||||
|
SchemeCommenter: true,
|
||||||
|
SchemeEditor: true,
|
||||||
|
SchemeAdmin: false,
|
||||||
|
}
|
||||||
|
guestMember, err := th.Server.App().AddMemberToBoard(newGuestMember)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, guestMember)
|
||||||
|
require.True(t, guestMember.SchemeViewer)
|
||||||
|
require.True(t, guestMember.SchemeCommenter)
|
||||||
|
require.True(t, guestMember.SchemeEditor)
|
||||||
|
require.False(t, guestMember.SchemeAdmin)
|
||||||
|
|
||||||
|
memberUpdate := &model.BoardMember{
|
||||||
|
UserID: userGuest,
|
||||||
|
BoardID: board.ID,
|
||||||
|
SchemeAdmin: true,
|
||||||
|
SchemeViewer: true,
|
||||||
|
SchemeCommenter: true,
|
||||||
|
SchemeEditor: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedGuestMember, resp := clients.Admin.UpdateBoardMember(memberUpdate)
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.True(t, updatedGuestMember.SchemeViewer)
|
||||||
|
require.True(t, updatedGuestMember.SchemeCommenter)
|
||||||
|
require.True(t, updatedGuestMember.SchemeEditor)
|
||||||
|
require.False(t, updatedGuestMember.SchemeAdmin)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteMember(t *testing.T) {
|
func TestDeleteMember(t *testing.T) {
|
||||||
|
@ -37,6 +37,7 @@ const (
|
|||||||
userCommenter string = "commenter"
|
userCommenter string = "commenter"
|
||||||
userEditor string = "editor"
|
userEditor string = "editor"
|
||||||
userAdmin string = "admin"
|
userAdmin string = "admin"
|
||||||
|
userGuest string = "guest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -47,6 +48,7 @@ var (
|
|||||||
userCommenterID = userCommenter
|
userCommenterID = userCommenter
|
||||||
userEditorID = userEditor
|
userEditorID = userEditor
|
||||||
userAdminID = userAdmin
|
userAdminID = userAdmin
|
||||||
|
userGuestID = userGuest
|
||||||
)
|
)
|
||||||
|
|
||||||
type LicenseType int
|
type LicenseType int
|
||||||
@ -62,6 +64,8 @@ type TestHelper struct {
|
|||||||
Server *server.Server
|
Server *server.Server
|
||||||
Client *client.Client
|
Client *client.Client
|
||||||
Client2 *client.Client
|
Client2 *client.Client
|
||||||
|
|
||||||
|
origEnvUnitTesting string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FakePermissionPluginAPI struct{}
|
type FakePermissionPluginAPI struct{}
|
||||||
@ -248,8 +252,16 @@ func newTestServerLocalMode() *server.Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
||||||
|
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||||
|
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||||
|
|
||||||
sessionToken := "TESTTOKEN"
|
sessionToken := "TESTTOKEN"
|
||||||
th := &TestHelper{T: t}
|
|
||||||
|
th := &TestHelper{
|
||||||
|
T: t,
|
||||||
|
origEnvUnitTesting: origUnitTesting,
|
||||||
|
}
|
||||||
|
|
||||||
th.Server = newTestServer(sessionToken)
|
th.Server = newTestServer(sessionToken)
|
||||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||||
@ -261,21 +273,42 @@ func SetupTestHelper(t *testing.T) *TestHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
|
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
|
||||||
th := &TestHelper{T: t}
|
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||||
|
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||||
|
|
||||||
|
th := &TestHelper{
|
||||||
|
T: t,
|
||||||
|
origEnvUnitTesting: origUnitTesting,
|
||||||
|
}
|
||||||
|
|
||||||
th.Server = NewTestServerPluginMode()
|
th.Server = NewTestServerPluginMode()
|
||||||
th.Start()
|
th.Start()
|
||||||
return th
|
return th
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupTestHelperLocalMode(t *testing.T) *TestHelper {
|
func SetupTestHelperLocalMode(t *testing.T) *TestHelper {
|
||||||
th := &TestHelper{T: t}
|
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||||
|
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||||
|
|
||||||
|
th := &TestHelper{
|
||||||
|
T: t,
|
||||||
|
origEnvUnitTesting: origUnitTesting,
|
||||||
|
}
|
||||||
|
|
||||||
th.Server = newTestServerLocalMode()
|
th.Server = newTestServerLocalMode()
|
||||||
th.Start()
|
th.Start()
|
||||||
return th
|
return th
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
||||||
th := &TestHelper{T: t}
|
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||||
|
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||||
|
|
||||||
|
th := &TestHelper{
|
||||||
|
T: t,
|
||||||
|
origEnvUnitTesting: origUnitTesting,
|
||||||
|
}
|
||||||
|
|
||||||
th.Server = newTestServerWithLicense("", licenseType)
|
th.Server = newTestServerWithLicense("", licenseType)
|
||||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
|
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
|
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||||
@ -343,6 +376,8 @@ func (th *TestHelper) InitBasic() *TestHelper {
|
|||||||
var ErrRegisterFail = errors.New("register failed")
|
var ErrRegisterFail = errors.New("register failed")
|
||||||
|
|
||||||
func (th *TestHelper) TearDown() {
|
func (th *TestHelper) TearDown() {
|
||||||
|
os.Setenv("FOCALBOARD_UNIT_TESTING", th.origEnvUnitTesting)
|
||||||
|
|
||||||
logger := th.Server.Logger()
|
logger := th.Server.Logger()
|
||||||
|
|
||||||
if l, ok := logger.(*mlog.Logger); ok {
|
if l, ok := logger.(*mlog.Logger); ok {
|
||||||
|
@ -54,7 +54,7 @@ func TestExportBoard(t *testing.T) {
|
|||||||
require.NoError(t, resp.Error)
|
require.NoError(t, resp.Error)
|
||||||
|
|
||||||
// check for test card
|
// check for test card
|
||||||
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID)
|
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, boardsImported, 1)
|
require.Len(t, boardsImported, 1)
|
||||||
boardImported := boardsImported[0]
|
boardImported := boardsImported[0]
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -73,6 +73,15 @@ func NewPluginTestStore(innerStore store.Store) *PluginTestStore {
|
|||||||
CreateAt: model.GetMillis(),
|
CreateAt: model.GetMillis(),
|
||||||
UpdateAt: model.GetMillis(),
|
UpdateAt: model.GetMillis(),
|
||||||
},
|
},
|
||||||
|
"guest": {
|
||||||
|
ID: "guest",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "guest",
|
||||||
|
Email: "guest@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
IsGuest: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
|
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
|
||||||
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
|
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
|
||||||
@ -109,6 +118,8 @@ func (s *PluginTestStore) GetTeamsForUser(userID string) ([]*model.Team, error)
|
|||||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
case "admin":
|
case "admin":
|
||||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
case "guest":
|
||||||
|
return []*model.Team{s.testTeam}, nil
|
||||||
}
|
}
|
||||||
return nil, errTestStore
|
return nil, errTestStore
|
||||||
}
|
}
|
||||||
@ -160,7 +171,16 @@ func (s *PluginTestStore) PatchUserProps(userID string, patch model.UserPropPatc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
func (s *PluginTestStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||||
|
if asGuestID == "guest" {
|
||||||
|
return []*model.User{
|
||||||
|
s.users["viewer"],
|
||||||
|
s.users["commenter"],
|
||||||
|
s.users["editor"],
|
||||||
|
s.users["admin"],
|
||||||
|
s.users["guest"],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case teamID == s.testTeam.ID:
|
case teamID == s.testTeam.ID:
|
||||||
return []*model.User{
|
return []*model.User{
|
||||||
@ -169,6 +189,7 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
|||||||
s.users["commenter"],
|
s.users["commenter"],
|
||||||
s.users["editor"],
|
s.users["editor"],
|
||||||
s.users["admin"],
|
s.users["admin"],
|
||||||
|
s.users["guest"],
|
||||||
}, nil
|
}, nil
|
||||||
case teamID == s.otherTeam.ID:
|
case teamID == s.otherTeam.ID:
|
||||||
return []*model.User{
|
return []*model.User{
|
||||||
@ -184,9 +205,9 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
|||||||
return nil, errTestStore
|
return nil, errTestStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||||
users := []*model.User{}
|
users := []*model.User{}
|
||||||
teamUsers, err := s.GetUsersByTeam(teamID)
|
teamUsers, err := s.GetUsersByTeam(teamID, asGuestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -199,6 +220,32 @@ func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) (
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||||
|
user, err := s.GetUserByID(seerID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !user.IsGuest {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
seerMembers, err := s.GetMembersForUser(seerID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
seenMembers, err := s.GetMembersForUser(seenID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, seerMember := range seerMembers {
|
||||||
|
for _, seenMember := range seenMembers {
|
||||||
|
if seerMember.BoardID == seenMember.BoardID {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PluginTestStore) SearchUserChannels(teamID, userID, query string) ([]*mmModel.Channel, error) {
|
func (s *PluginTestStore) SearchUserChannels(teamID, userID, query string) ([]*mmModel.Channel, error) {
|
||||||
return []*mmModel.Channel{
|
return []*mmModel.Channel{
|
||||||
{
|
{
|
||||||
@ -235,8 +282,8 @@ func (s *PluginTestStore) GetChannel(teamID, channel string) (*mmModel.Channel,
|
|||||||
return nil, errTestStore
|
return nil, errTestStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
|
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
boards, err := s.Store.SearchBoardsForUser(term, userID)
|
boards, err := s.Store.SearchBoardsForUser(term, userID, includePublicBoards)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,20 @@ func (mr *MockServicesAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePreferencesForUser mocks base method.
|
||||||
|
func (m *MockServicesAPI) DeletePreferencesForUser(arg0 string, arg1 model.Preferences) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser.
|
||||||
|
func (mr *MockServicesAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).DeletePreferencesForUser), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureBot mocks base method.
|
// EnsureBot mocks base method.
|
||||||
func (m *MockServicesAPI) EnsureBot(arg0 *model.Bot) (string, error) {
|
func (m *MockServicesAPI) EnsureBot(arg0 *model.Bot) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -243,6 +257,21 @@ func (mr *MockServicesAPIMockRecorder) GetMasterDB() *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockServicesAPI)(nil).GetMasterDB))
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockServicesAPI)(nil).GetMasterDB))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPreferencesForUser mocks base method.
|
||||||
|
func (m *MockServicesAPI) GetPreferencesForUser(arg0 string) (model.Preferences, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0)
|
||||||
|
ret0, _ := ret[0].(model.Preferences)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPreferencesForUser indicates an expected call of GetPreferencesForUser.
|
||||||
|
func (mr *MockServicesAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetPreferencesForUser), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// GetTeamMember mocks base method.
|
// GetTeamMember mocks base method.
|
||||||
func (m *MockServicesAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, error) {
|
func (m *MockServicesAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -399,6 +428,20 @@ func (mr *MockServicesAPIMockRecorder) RegisterRouter(arg0 interface{}) *gomock.
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatePreferencesForUser mocks base method.
|
||||||
|
func (m *MockServicesAPI) UpdatePreferencesForUser(arg0 string, arg1 model.Preferences) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser.
|
||||||
|
func (mr *MockServicesAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdatePreferencesForUser), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUser mocks base method.
|
// UpdateUser mocks base method.
|
||||||
func (m *MockServicesAPI) UpdateUser(arg0 *model.User) (*model.User, error) {
|
func (m *MockServicesAPI) UpdateUser(arg0 *model.User) (*model.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -17,4 +17,6 @@ var (
|
|||||||
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
|
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
|
||||||
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
|
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
|
||||||
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
|
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
|
||||||
|
PermissionCommentBoardCards = &mmModel.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
|
||||||
|
PermissionDeleteOthersComments = &mmModel.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
|
||||||
)
|
)
|
||||||
|
@ -85,4 +85,9 @@ type ServicesAPI interface {
|
|||||||
|
|
||||||
// Router service
|
// Router service
|
||||||
RegisterRouter(sub *mux.Router)
|
RegisterRouter(sub *mux.Router)
|
||||||
|
|
||||||
|
// Preferences services
|
||||||
|
GetPreferencesForUser(userID string) (mm_model.Preferences, error)
|
||||||
|
UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error
|
||||||
|
DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error
|
||||||
}
|
}
|
||||||
|
@ -67,10 +67,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch permission {
|
switch permission {
|
||||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||||
return member.SchemeAdmin
|
return member.SchemeAdmin
|
||||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||||
return member.SchemeAdmin || member.SchemeEditor
|
return member.SchemeAdmin || member.SchemeEditor
|
||||||
|
case model.PermissionCommentBoardCards:
|
||||||
|
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||||
case model.PermissionViewBoard:
|
case model.PermissionViewBoard:
|
||||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||||
default:
|
default:
|
||||||
|
@ -99,10 +99,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch permission {
|
switch permission {
|
||||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||||
return member.SchemeAdmin
|
return member.SchemeAdmin
|
||||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||||
return member.SchemeAdmin || member.SchemeEditor
|
return member.SchemeAdmin || member.SchemeEditor
|
||||||
|
case model.PermissionCommentBoardCards:
|
||||||
|
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||||
case model.PermissionViewBoard:
|
case model.PermissionViewBoard:
|
||||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||||
default:
|
default:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||||
package mmpermissions
|
package mmpermissions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
|
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
@ -71,8 +71,7 @@ func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
|
|||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("count(*)").
|
Select("count(*)").
|
||||||
From("Users").
|
From("Users").
|
||||||
Where(sq.Eq{"deleteAt": 0}).
|
Where(sq.Eq{"deleteAt": 0})
|
||||||
Where(sq.NotEq{"roles": "system_guest"})
|
|
||||||
row := query.QueryRow()
|
row := query.QueryRow()
|
||||||
|
|
||||||
var count int
|
var count int
|
||||||
@ -267,16 +266,32 @@ func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
|
|||||||
return builder.RunWith(s.mmDB)
|
return builder.RunWith(s.mmDB)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
||||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
|
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
|
||||||
From("Users as u").
|
From("Users as u").
|
||||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
|
||||||
LeftJoin("Bots b ON ( b.UserID = u.id )").
|
LeftJoin("Bots b ON ( b.UserID = u.id )").
|
||||||
Where(sq.Eq{"u.deleteAt": 0}).
|
Where(sq.Eq{"u.deleteAt": 0})
|
||||||
Where(sq.NotEq{"u.roles": "system_guest"}).
|
|
||||||
Where(sq.Eq{"tm.TeamId": teamID})
|
if asGuestID == "" {
|
||||||
|
query = query.
|
||||||
|
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||||
|
Where(sq.Eq{"tm.TeamId": teamID})
|
||||||
|
} else {
|
||||||
|
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
boardsIDs := []string{}
|
||||||
|
for _, board := range boards {
|
||||||
|
boardsIDs = append(boardsIDs, board.ID)
|
||||||
|
}
|
||||||
|
query = query.
|
||||||
|
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
|
||||||
|
Where(sq.Eq{"bm.BoardId": boardsIDs})
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := query.Query()
|
rows, err := query.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -314,12 +329,11 @@ func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, err
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
||||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
|
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
|
||||||
From("Users as u").
|
From("Users as u").
|
||||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
|
||||||
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||||
Where(sq.Eq{"u.deleteAt": 0}).
|
Where(sq.Eq{"u.deleteAt": 0}).
|
||||||
Where(sq.Or{
|
Where(sq.Or{
|
||||||
@ -328,11 +342,27 @@ func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery strin
|
|||||||
sq.Like{"u.firstname": "%" + searchQuery + "%"},
|
sq.Like{"u.firstname": "%" + searchQuery + "%"},
|
||||||
sq.Like{"u.lastname": "%" + searchQuery + "%"},
|
sq.Like{"u.lastname": "%" + searchQuery + "%"},
|
||||||
}).
|
}).
|
||||||
Where(sq.Eq{"tm.TeamId": teamID}).
|
|
||||||
Where(sq.NotEq{"u.roles": "system_guest"}).
|
|
||||||
OrderBy("u.username").
|
OrderBy("u.username").
|
||||||
Limit(10)
|
Limit(10)
|
||||||
|
|
||||||
|
if asGuestID == "" {
|
||||||
|
query = query.
|
||||||
|
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||||
|
Where(sq.Eq{"tm.TeamId": teamID})
|
||||||
|
} else {
|
||||||
|
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
boardsIDs := []string{}
|
||||||
|
for _, board := range boards {
|
||||||
|
boardsIDs = append(boardsIDs, board.ID)
|
||||||
|
}
|
||||||
|
query = query.
|
||||||
|
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
|
||||||
|
Where(sq.Eq{"bm.BoardId": boardsIDs})
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := query.Query()
|
rows, err := query.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -366,6 +396,7 @@ func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, erro
|
|||||||
&user.UpdateAt,
|
&user.UpdateAt,
|
||||||
&user.DeleteAt,
|
&user.DeleteAt,
|
||||||
&user.IsBot,
|
&user.IsBot,
|
||||||
|
&user.IsGuest,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -558,7 +589,7 @@ func boardFields(prefix string) []string {
|
|||||||
// term that are either private and which the user is a member of, or
|
// term that are either private and which the user is a member of, or
|
||||||
// they're open, regardless of the user membership.
|
// they're open, regardless of the user membership.
|
||||||
// Search is case-insensitive.
|
// Search is case-insensitive.
|
||||||
func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string) ([]*model.Board, error) {
|
func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
query := s.getQueryBuilder().
|
query := s.getQueryBuilder().
|
||||||
Select(boardFields("b.")...).
|
Select(boardFields("b.")...).
|
||||||
Distinct().
|
Distinct().
|
||||||
@ -568,17 +599,20 @@ func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string) ([]*model
|
|||||||
LeftJoin("ChannelMembers as cm on cm.channelId=b.channel_id").
|
LeftJoin("ChannelMembers as cm on cm.channelId=b.channel_id").
|
||||||
Where(sq.Eq{"b.is_template": false}).
|
Where(sq.Eq{"b.is_template": false}).
|
||||||
Where(sq.Eq{"tm.userID": userID}).
|
Where(sq.Eq{"tm.userID": userID}).
|
||||||
Where(sq.Eq{"tm.deleteAt": 0}).
|
Where(sq.Eq{"tm.deleteAt": 0})
|
||||||
Where(sq.Or{
|
|
||||||
|
if includePublicBoards {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||||
sq.And{
|
sq.Eq{"bm.user_id": userID},
|
||||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
sq.Eq{"cm.userId": userID},
|
||||||
sq.Or{
|
|
||||||
sq.Eq{"bm.user_id": userID},
|
|
||||||
sq.Eq{"cm.userId": userID},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Eq{"bm.user_id": userID},
|
||||||
|
sq.Eq{"cm.userId": userID},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if term != "" {
|
if term != "" {
|
||||||
// break search query into space separated words
|
// break search query into space separated words
|
||||||
@ -799,12 +833,14 @@ func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.Board
|
|||||||
return members, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
|
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
members, err := s.GetMembersForUser(userID)
|
members, err := s.GetMembersForUser(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Handle the includePublicBoards
|
||||||
|
|
||||||
boardIDs := []string{}
|
boardIDs := []string{}
|
||||||
for _, m := range members {
|
for _, m := range members {
|
||||||
boardIDs = append(boardIDs, m.BoardID)
|
boardIDs = append(boardIDs, m.BoardID)
|
||||||
@ -919,3 +955,67 @@ func (s *MattermostAuthLayer) GetUserTimezone(userID string) (string, error) {
|
|||||||
timezone := user.Timezone
|
timezone := user.Timezone
|
||||||
return mmModel.GetPreferredTimezone(timezone), nil
|
return mmModel.GetPreferredTimezone(timezone), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MattermostAuthLayer) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||||
|
mmuser, appErr := s.servicesAPI.GetUserByID(seerID)
|
||||||
|
if appErr != nil {
|
||||||
|
return false, appErr
|
||||||
|
}
|
||||||
|
if !mmuser.IsGuest() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Select("1").
|
||||||
|
From(s.tablePrefix + "board_members AS BM1").
|
||||||
|
Join(s.tablePrefix + "board_members AS BM2 ON BM1.BoardID=BM2.BoardID").
|
||||||
|
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||||
|
Where(sq.Or{
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"BM1.UserID": seerID},
|
||||||
|
sq.Eq{"BM2.UserID": seenID},
|
||||||
|
},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"BM1.UserID": seenID},
|
||||||
|
sq.Eq{"BM2.UserID": seerID},
|
||||||
|
},
|
||||||
|
}).Limit(1)
|
||||||
|
|
||||||
|
rows, err := query.Query()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer s.CloseRows(rows)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query = s.getQueryBuilder().
|
||||||
|
Select("1").
|
||||||
|
From("ChannelMembers AS CM1").
|
||||||
|
Join("ChannelMembers AS CM2 ON CM1.BoardID=CM2.BoardID").
|
||||||
|
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||||
|
Where(sq.Or{
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"CM1.UserID": seerID},
|
||||||
|
sq.Eq{"CM2.UserID": seenID},
|
||||||
|
},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"CM1.UserID": seenID},
|
||||||
|
sq.Eq{"CM2.UserID": seerID},
|
||||||
|
},
|
||||||
|
}).Limit(1)
|
||||||
|
|
||||||
|
rows, err = query.Query()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer s.CloseRows(rows)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
@ -50,6 +50,21 @@ func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1, arg2 interfa
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanSeeUser mocks base method.
|
||||||
|
func (m *MockStore) CanSeeUser(arg0, arg1 string) (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CanSeeUser", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanSeeUser indicates an expected call of CanSeeUser.
|
||||||
|
func (mr *MockStoreMockRecorder) CanSeeUser(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSeeUser", reflect.TypeOf((*MockStore)(nil).CanSeeUser), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// CleanUpSessions mocks base method.
|
// CleanUpSessions mocks base method.
|
||||||
func (m *MockStore) CleanUpSessions(arg0 int64) error {
|
func (m *MockStore) CleanUpSessions(arg0 int64) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -429,21 +444,6 @@ func (mr *MockStoreMockRecorder) GetBlocksForBoard(arg0 interface{}) *gomock.Cal
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlocksWithBoardID mocks base method.
|
|
||||||
func (m *MockStore) GetBlocksWithBoardID(arg0 string) ([]model.Block, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetBlocksWithBoardID", arg0)
|
|
||||||
ret0, _ := ret[0].([]model.Block)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlocksWithBoardID indicates an expected call of GetBlocksWithBoardID.
|
|
||||||
func (mr *MockStoreMockRecorder) GetBlocksWithBoardID(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithBoardID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithBoardID), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlocksWithParent mocks base method.
|
// GetBlocksWithParent mocks base method.
|
||||||
func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]model.Block, error) {
|
func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]model.Block, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@ -567,18 +567,18 @@ func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBoardsForUserAndTeam mocks base method.
|
// GetBoardsForUserAndTeam mocks base method.
|
||||||
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) {
|
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1)
|
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].([]*model.Board)
|
ret0, _ := ret[0].([]*model.Board)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam.
|
// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam.
|
||||||
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBoardsInTeamByIds mocks base method.
|
// GetBoardsInTeamByIds mocks base method.
|
||||||
@ -1091,18 +1091,18 @@ func (mr *MockStoreMockRecorder) GetUserTimezone(arg0 interface{}) *gomock.Call
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersByTeam mocks base method.
|
// GetUsersByTeam mocks base method.
|
||||||
func (m *MockStore) GetUsersByTeam(arg0 string) ([]*model.User, error) {
|
func (m *MockStore) GetUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0)
|
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]*model.User)
|
ret0, _ := ret[0].([]*model.User)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersByTeam indicates an expected call of GetUsersByTeam.
|
// GetUsersByTeam indicates an expected call of GetUsersByTeam.
|
||||||
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0 interface{}) *gomock.Call {
|
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersList mocks base method.
|
// GetUsersList mocks base method.
|
||||||
@ -1338,18 +1338,18 @@ func (mr *MockStoreMockRecorder) SaveMember(arg0 interface{}) *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SearchBoardsForUser mocks base method.
|
// SearchBoardsForUser mocks base method.
|
||||||
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string) ([]*model.Board, error) {
|
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1)
|
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].([]*model.Board)
|
ret0, _ := ret[0].([]*model.Board)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchBoardsForUser indicates an expected call of SearchBoardsForUser.
|
// SearchBoardsForUser indicates an expected call of SearchBoardsForUser.
|
||||||
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchBoardsForUserInTeam mocks base method.
|
// SearchBoardsForUserInTeam mocks base method.
|
||||||
@ -1383,18 +1383,18 @@ func (mr *MockStoreMockRecorder) SearchUserChannels(arg0, arg1, arg2 interface{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsersByTeam mocks base method.
|
// SearchUsersByTeam mocks base method.
|
||||||
func (m *MockStore) SearchUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
|
func (m *MockStore) SearchUsersByTeam(arg0, arg1, arg2 string) ([]*model.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1)
|
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].([]*model.User)
|
ret0, _ := ret[0].([]*model.User)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchUsersByTeam indicates an expected call of SearchUsersByTeam.
|
// SearchUsersByTeam indicates an expected call of SearchUsersByTeam.
|
||||||
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
|
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMessage mocks base method.
|
// SendMessage mocks base method.
|
||||||
|
@ -252,21 +252,25 @@ func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, err
|
|||||||
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
|
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
query := s.getQueryBuilder(db).
|
query := s.getQueryBuilder(db).
|
||||||
Select(boardFields("b.")...).
|
Select(boardFields("b.")...).
|
||||||
Distinct().
|
Distinct().
|
||||||
From(s.tablePrefix + "boards as b").
|
From(s.tablePrefix + "boards as b").
|
||||||
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||||
Where(sq.Eq{"b.team_id": teamID}).
|
Where(sq.Eq{"b.team_id": teamID}).
|
||||||
Where(sq.Eq{"b.is_template": false}).
|
Where(sq.Eq{"b.is_template": false})
|
||||||
Where(sq.Or{
|
|
||||||
|
if includePublicBoards {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||||
sq.And{
|
sq.Eq{"bm.user_id": userID},
|
||||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
|
||||||
sq.Eq{"bm.user_id": userID},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Eq{"bm.user_id": userID},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := query.Query()
|
rows, err := query.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -643,20 +647,24 @@ func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*mode
|
|||||||
// term that are either private and which the user is a member of, or
|
// term that are either private and which the user is a member of, or
|
||||||
// they're open, regardless of the user membership.
|
// they're open, regardless of the user membership.
|
||||||
// Search is case-insensitive.
|
// Search is case-insensitive.
|
||||||
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string) ([]*model.Board, error) {
|
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
query := s.getQueryBuilder(db).
|
query := s.getQueryBuilder(db).
|
||||||
Select(boardFields("b.")...).
|
Select(boardFields("b.")...).
|
||||||
Distinct().
|
Distinct().
|
||||||
From(s.tablePrefix + "boards as b").
|
From(s.tablePrefix + "boards as b").
|
||||||
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||||
Where(sq.Eq{"b.is_template": false}).
|
Where(sq.Eq{"b.is_template": false})
|
||||||
Where(sq.Or{
|
|
||||||
|
if includePublicBoards {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||||
sq.And{
|
sq.Eq{"bm.user_id": userID},
|
||||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
|
||||||
sq.Eq{"bm.user_id": userID},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
query = query.Where(sq.Or{
|
||||||
|
sq.Eq{"bm.user_id": userID},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if term != "" {
|
if term != "" {
|
||||||
// break search query into space separated words
|
// break search query into space separated words
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
DROP INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions;
|
||||||
|
DROP INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks;
|
||||||
|
|
||||||
|
{{if .mysql}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
|
||||||
|
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .postgres}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
|
||||||
|
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .sqlite}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
|
||||||
|
|
||||||
|
CREATE TABLE {{.prefix}}blocks (
|
||||||
|
id VARCHAR(36),
|
||||||
|
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||||
|
parent_id VARCHAR(36),
|
||||||
|
schema BIGINT,
|
||||||
|
type TEXT,
|
||||||
|
title TEXT,
|
||||||
|
fields TEXT,
|
||||||
|
create_at BIGINT,
|
||||||
|
update_at BIGINT,
|
||||||
|
delete_at BIGINT,
|
||||||
|
root_id VARCHAR(36),
|
||||||
|
modified_by VARCHAR(36),
|
||||||
|
channel_id VARCHAR(36),
|
||||||
|
created_by VARCHAR(36),
|
||||||
|
board_id VARCHAR(36),
|
||||||
|
PRIMARY KEY (channel_id, id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
|
||||||
|
|
||||||
|
DROP TABLE {{.prefix}}blocks_tmp;
|
||||||
|
{{end}}
|
@ -0,0 +1,44 @@
|
|||||||
|
{{- /* delete old blocks PK and add id as the new one */ -}}
|
||||||
|
{{if .mysql}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
|
||||||
|
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .postgres}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
|
||||||
|
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{- /* there is no way for SQLite to update the PK or add a unique constraint */ -}}
|
||||||
|
{{if .sqlite}}
|
||||||
|
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
|
||||||
|
|
||||||
|
CREATE TABLE {{.prefix}}blocks (
|
||||||
|
id VARCHAR(36),
|
||||||
|
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||||
|
parent_id VARCHAR(36),
|
||||||
|
schema BIGINT,
|
||||||
|
type TEXT,
|
||||||
|
title TEXT,
|
||||||
|
fields TEXT,
|
||||||
|
create_at BIGINT,
|
||||||
|
update_at BIGINT,
|
||||||
|
delete_at BIGINT,
|
||||||
|
root_id VARCHAR(36),
|
||||||
|
modified_by VARCHAR(36),
|
||||||
|
channel_id VARCHAR(36),
|
||||||
|
created_by VARCHAR(36),
|
||||||
|
board_id VARCHAR(36),
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
|
||||||
|
|
||||||
|
DROP TABLE {{.prefix}}blocks_tmp;
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{- /* most block searches use board_id or a combination of board and parent ids */ -}}
|
||||||
|
CREATE INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks (board_id, parent_id);
|
||||||
|
|
||||||
|
{{- /* get subscriptions is used once per board page load */ -}}
|
||||||
|
CREATE INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions (subscriber_id);
|
@ -46,6 +46,11 @@ func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, bloc
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||||
|
return s.canSeeUser(s.db, seerID, seenID)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) CleanUpSessions(expireTime int64) error {
|
func (s *SQLStore) CleanUpSessions(expireTime int64) error {
|
||||||
return s.cleanUpSessions(s.db, expireTime)
|
return s.cleanUpSessions(s.db, expireTime)
|
||||||
|
|
||||||
@ -344,8 +349,8 @@ func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit ui
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) {
|
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
return s.getBoardsForUserAndTeam(s.db, userID, teamID)
|
return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,8 +524,8 @@ func (s *SQLStore) GetUserTimezone(userID string) (string, error) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
func (s *SQLStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||||
return s.getUsersByTeam(s.db, teamID)
|
return s.getUsersByTeam(s.db, teamID, asGuestID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,8 +761,8 @@ func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
|
func (s *SQLStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||||
return s.searchBoardsForUser(s.db, term, userID)
|
return s.searchBoardsForUser(s.db, term, userID, includePublicBoards)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,8 +776,8 @@ func (s *SQLStore) SearchUserChannels(teamID string, userID string, query string
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||||
return s.searchUsersByTeam(s.db, teamID, searchQuery)
|
return s.searchUsersByTeam(s.db, teamID, searchQuery, asGuestID)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ package sqlstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
|
||||||
@ -15,6 +17,9 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
var ErrInvalidMariaDB = errors.New("MariaDB database is not supported, you can find more information at https://docs.mattermost.com/install/software-hardware-requirements.html#database-software")
|
||||||
|
|
||||||
// SQLStore is a SQL database.
|
// SQLStore is a SQL database.
|
||||||
type SQLStore struct {
|
type SQLStore struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
@ -53,6 +58,10 @@ func New(params Params) (*SQLStore, error) {
|
|||||||
servicesAPI: params.ServicesAPI,
|
servicesAPI: params.ServicesAPI,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if store.IsMariaDB() {
|
||||||
|
return nil, ErrInvalidMariaDB
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
store.isBinaryParam, err = store.computeBinaryParam()
|
store.isBinaryParam, err = store.computeBinaryParam()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -70,6 +79,22 @@ func New(params Params) (*SQLStore, error) {
|
|||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) IsMariaDB() bool {
|
||||||
|
if s.dbType != model.MysqlDBType {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
row := s.db.QueryRow("SELECT Version()")
|
||||||
|
|
||||||
|
var version string
|
||||||
|
if err := row.Scan(&version); err != nil {
|
||||||
|
s.logger.Error("error checking database version", mlog.Err(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(strings.ToLower(version), "mariadb")
|
||||||
|
}
|
||||||
|
|
||||||
// computeBinaryParam returns whether the data source uses binary_parameters
|
// computeBinaryParam returns whether the data source uses binary_parameters
|
||||||
// when using Postgres.
|
// when using Postgres.
|
||||||
func (s *SQLStore) computeBinaryParam() (bool, error) {
|
func (s *SQLStore) computeBinaryParam() (bool, error) {
|
||||||
|
@ -211,11 +211,11 @@ func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string) ([]*model.User, error) {
|
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string) ([]*model.User, error) {
|
||||||
return s.getUsersByCondition(db, nil, 0)
|
return s.getUsersByCondition(db, nil, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string) ([]*model.User, error) {
|
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string) ([]*model.User, error) {
|
||||||
return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
|
return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +275,10 @@ func (s *SQLStore) patchUserProps(db sq.BaseRunner, userID string, patch model.U
|
|||||||
return s.updateUser(db, user)
|
return s.updateUser(db, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) canSeeUser(db sq.BaseRunner, seerID string, seenID string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) sendMessage(db sq.BaseRunner, message, postType string, receipts []string) error {
|
func (s *SQLStore) sendMessage(db sq.BaseRunner, message, postType string, receipts []string) error {
|
||||||
return errUnsupportedOperation
|
return errUnsupportedOperation
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mockstore/mockstore.go -package mockstore . Store
|
//go:generate mockgen -destination=mockstore/mockstore.go -package mockstore . Store
|
||||||
//go:generate go run ./generators/main.go
|
//go:generate go run ./generators/main.go
|
||||||
package store
|
package store
|
||||||
|
|
||||||
@ -61,8 +61,8 @@ type Store interface {
|
|||||||
UpdateUser(user *model.User) error
|
UpdateUser(user *model.User) error
|
||||||
UpdateUserPassword(username, password string) error
|
UpdateUserPassword(username, password string) error
|
||||||
UpdateUserPasswordByID(userID, password string) error
|
UpdateUserPasswordByID(userID, password string) error
|
||||||
GetUsersByTeam(teamID string) ([]*model.User, error)
|
GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error)
|
||||||
SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error)
|
SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error)
|
||||||
PatchUserProps(userID string, patch model.UserPropPatch) error
|
PatchUserProps(userID string, patch model.UserPropPatch) error
|
||||||
|
|
||||||
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
|
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
|
||||||
@ -89,7 +89,7 @@ type Store interface {
|
|||||||
// @withTransaction
|
// @withTransaction
|
||||||
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
|
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
|
||||||
GetBoard(id string) (*model.Board, error)
|
GetBoard(id string) (*model.Board, error)
|
||||||
GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error)
|
GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error)
|
||||||
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
|
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
|
||||||
// @withTransaction
|
// @withTransaction
|
||||||
DeleteBoard(boardID, userID string) error
|
DeleteBoard(boardID, userID string) error
|
||||||
@ -100,7 +100,8 @@ type Store interface {
|
|||||||
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
|
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
|
||||||
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
|
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
|
||||||
GetMembersForUser(userID string) ([]*model.BoardMember, error)
|
GetMembersForUser(userID string) ([]*model.BoardMember, error)
|
||||||
SearchBoardsForUser(term, userID string) ([]*model.Board, error)
|
CanSeeUser(seerID string, seenID string) (bool, error)
|
||||||
|
SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error)
|
||||||
SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error)
|
SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error)
|
||||||
|
|
||||||
// @withTransaction
|
// @withTransaction
|
||||||
|
@ -952,12 +952,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get block history after updateAt", func(t *testing.T) {
|
t.Run("get block history after updateAt", func(t *testing.T) {
|
||||||
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
|
rBlock, err2 := store.GetBlock("block3")
|
||||||
require.NoError(t, err2)
|
require.NoError(t, err2)
|
||||||
require.NotZero(t, rBlocks[2].UpdateAt)
|
require.NotZero(t, rBlock.UpdateAt)
|
||||||
|
|
||||||
opts := model.QueryBlockHistoryOptions{
|
opts := model.QueryBlockHistoryOptions{
|
||||||
AfterUpdateAt: rBlocks[2].UpdateAt,
|
AfterUpdateAt: rBlock.UpdateAt,
|
||||||
Descending: false,
|
Descending: false,
|
||||||
}
|
}
|
||||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||||
@ -970,12 +970,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("get block history before updateAt", func(t *testing.T) {
|
t.Run("get block history before updateAt", func(t *testing.T) {
|
||||||
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
|
rBlock, err2 := store.GetBlock("block3")
|
||||||
require.NoError(t, err2)
|
require.NoError(t, err2)
|
||||||
require.NotZero(t, rBlocks[2].UpdateAt)
|
require.NotZero(t, rBlock.UpdateAt)
|
||||||
|
|
||||||
opts := model.QueryBlockHistoryOptions{
|
opts := model.QueryBlockHistoryOptions{
|
||||||
BeforeUpdateAt: rBlocks[2].UpdateAt,
|
BeforeUpdateAt: rBlock.UpdateAt,
|
||||||
Descending: true,
|
Descending: true,
|
||||||
}
|
}
|
||||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||||
|
@ -68,8 +68,8 @@ func getBoardsInsightsTest(t *testing.T, store store.Store) {
|
|||||||
|
|
||||||
_, _ = store.SaveMember(bm)
|
_, _ = store.SaveMember(bm)
|
||||||
|
|
||||||
boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID)
|
boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID, true)
|
||||||
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID)
|
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID, true)
|
||||||
t.Run("team insights", func(t *testing.T) {
|
t.Run("team insights", func(t *testing.T) {
|
||||||
boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID}
|
boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID}
|
||||||
topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID, testUserID,
|
topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID, testUserID,
|
||||||
|
@ -168,7 +168,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("should only find the two boards that the user is a member of for team 1 plus the one open board", func(t *testing.T) {
|
t.Run("should only find the two boards that the user is a member of for team 1 plus the one open board", func(t *testing.T) {
|
||||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1)
|
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.ElementsMatch(t, []*model.Board{
|
require.ElementsMatch(t, []*model.Board{
|
||||||
rBoard1,
|
rBoard1,
|
||||||
@ -177,8 +177,17 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
}, boards)
|
}, boards)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should only find the two boards that the user is a member of for team 1", func(t *testing.T) {
|
||||||
|
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, []*model.Board{
|
||||||
|
rBoard1,
|
||||||
|
rBoard2,
|
||||||
|
}, boards)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) {
|
t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) {
|
||||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID2)
|
boards, err := store.GetBoardsForUserAndTeam(userID, teamID2, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, boards, 1)
|
require.Len(t, boards, 1)
|
||||||
require.Equal(t, board5.ID, boards[0].ID)
|
require.Equal(t, board5.ID, boards[0].ID)
|
||||||
@ -688,7 +697,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
userID := "user-id-1"
|
userID := "user-id-1"
|
||||||
|
|
||||||
t.Run("should return empty if user is not a member of any board and there are no public boards on the team", func(t *testing.T) {
|
t.Run("should return empty if user is not a member of any board and there are no public boards on the team", func(t *testing.T) {
|
||||||
boards, err := store.SearchBoardsForUser("", userID)
|
boards, err := store.SearchBoardsForUser("", userID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, boards)
|
require.Empty(t, boards)
|
||||||
})
|
})
|
||||||
@ -743,6 +752,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
TeamID string
|
TeamID string
|
||||||
UserID string
|
UserID string
|
||||||
Term string
|
Term string
|
||||||
|
IncludePublic bool
|
||||||
ExpectedBoardIDs []string
|
ExpectedBoardIDs []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -750,6 +760,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Term: "",
|
Term: "",
|
||||||
|
IncludePublic: true,
|
||||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -757,13 +768,23 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Term: "board",
|
Term: "board",
|
||||||
|
IncludePublic: true,
|
||||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "should find all with term board where the user is member of",
|
||||||
|
TeamID: teamID1,
|
||||||
|
UserID: userID,
|
||||||
|
Term: "board",
|
||||||
|
IncludePublic: false,
|
||||||
|
ExpectedBoardIDs: []string{board1.ID, board3.ID, board5.ID},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "should find only public as per the term, wether user is a member or not",
|
Name: "should find only public as per the term, wether user is a member or not",
|
||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Term: "public",
|
Term: "public",
|
||||||
|
IncludePublic: true,
|
||||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board5.ID},
|
ExpectedBoardIDs: []string{board1.ID, board2.ID, board5.ID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -771,6 +792,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Term: "priv",
|
Term: "priv",
|
||||||
|
IncludePublic: true,
|
||||||
ExpectedBoardIDs: []string{board3.ID},
|
ExpectedBoardIDs: []string{board3.ID},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -778,13 +800,14 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID2,
|
TeamID: teamID2,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Term: "non-matching-term",
|
Term: "non-matching-term",
|
||||||
|
IncludePublic: true,
|
||||||
ExpectedBoardIDs: []string{},
|
ExpectedBoardIDs: []string{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.Name, func(t *testing.T) {
|
t.Run(tc.Name, func(t *testing.T) {
|
||||||
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID)
|
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID, tc.IncludePublic)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
boardIDs := []string{}
|
boardIDs := []string{}
|
||||||
|
@ -41,7 +41,7 @@ func testCreateBoardsAndBlocks(t *testing.T, store store.Store) {
|
|||||||
teamID := testTeamID
|
teamID := testTeamID
|
||||||
userID := testUserID
|
userID := testUserID
|
||||||
|
|
||||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID)
|
boards, err := store.GetBoardsForUserAndTeam(userID, teamID, true)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Empty(t, boards)
|
require.Empty(t, boards)
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
|
|||||||
|
|
||||||
func testGetTeamUsers(t *testing.T, store store.Store) {
|
func testGetTeamUsers(t *testing.T, store store.Store) {
|
||||||
t.Run("GetTeamUSers", func(t *testing.T) {
|
t.Run("GetTeamUSers", func(t *testing.T) {
|
||||||
users, err := store.GetUsersByTeam("team_1")
|
users, err := store.GetUsersByTeam("team_1", "")
|
||||||
require.Equal(t, 0, len(users))
|
require.Equal(t, 0, len(users))
|
||||||
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
|
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ func testGetTeamUsers(t *testing.T, store store.Store) {
|
|||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
users, err = store.GetUsersByTeam("team_1")
|
users, err = store.GetUsersByTeam("team_1", "")
|
||||||
require.Equal(t, 1, len(users))
|
require.Equal(t, 1, len(users))
|
||||||
require.Equal(t, "darth.vader", users[0].Username)
|
require.Equal(t, "darth.vader", users[0].Username)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
|
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
|
||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -242,6 +242,7 @@
|
|||||||
"Sidebar.untitled-view": "(Untitled View)",
|
"Sidebar.untitled-view": "(Untitled View)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Move To...",
|
"SidebarCategories.BlocksMenu.Move": "Move To...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
|
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
|
||||||
|
"SidebarCategories.CategoryMenu.CreateBoard": "Create New Board",
|
||||||
"SidebarCategories.CategoryMenu.Delete": "Delete Category",
|
"SidebarCategories.CategoryMenu.Delete": "Delete Category",
|
||||||
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in <b>{categoryName}</b> will move back to the Boards categories. You're not removed from any boards.",
|
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Boards in <b>{categoryName}</b> will move back to the Boards categories. You're not removed from any boards.",
|
||||||
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?",
|
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Delete this category?",
|
||||||
@ -400,4 +401,4 @@
|
|||||||
"tutorial_tip.ok": "Next",
|
"tutorial_tip.ok": "Next",
|
||||||
"tutorial_tip.out": "Opt out of these tips.",
|
"tutorial_tip.out": "Opt out of these tips.",
|
||||||
"tutorial_tip.seen": "Seen this before?"
|
"tutorial_tip.seen": "Seen this before?"
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ exports[`/components/confirmationDialogBox confirmDialog should match snapshot 1
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -95,7 +95,7 @@ exports[`/components/confirmationDialogBox confirmDialog with Confirm Button Tex
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -20,7 +20,7 @@ exports[`components/dialog should match snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -61,7 +61,7 @@ exports[`components/dialog should return dialog and click on cancel button 1`] =
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -63,12 +63,13 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
|
|||||||
username: 'username_1',
|
username: 'username_1',
|
||||||
email: '',
|
email: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
firstname: '',
|
firstname: '',
|
||||||
lastname: '',
|
lastname: '',
|
||||||
props: {},
|
props: {},
|
||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const template1Title = 'Template 1'
|
const template1Title = 'Template 1'
|
||||||
@ -84,7 +85,7 @@ describe('components/boardTemplateSelector/boardTemplateSelector', () => {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
me,
|
me,
|
||||||
boardUsers: [me],
|
boardUsers: {[me.id]: me},
|
||||||
},
|
},
|
||||||
boards: {
|
boards: {
|
||||||
boards: [
|
boards: [
|
||||||
|
@ -96,16 +96,17 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const me: IUser = {
|
const me: IUser = {
|
||||||
id: 'user-id-1',
|
id: 'user-id-1',
|
||||||
username: 'username_1',
|
username: 'username_1',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
firstname: '',
|
firstname: '',
|
||||||
lastname: '',
|
lastname: '',
|
||||||
email: '',
|
email: '',
|
||||||
props: {},
|
props: {},
|
||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
background-color: rgba(var(--sidebar-text-rgb), 0.08);
|
background-color: rgba(var(--sidebar-text-rgb), 0.08);
|
||||||
color: rgba(var(--sidebar-text-rgb), 0.56);
|
color: rgba(var(--sidebar-text-rgb), 0.56);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px 6px 4px;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@ -28,9 +28,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-board-icon {
|
.add-board-icon {
|
||||||
margin-left: 4px;
|
border-radius: 28px;
|
||||||
|
margin-left: 8px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
flex: 0 0 28px;
|
flex: 0 0 28px;
|
||||||
|
background-color: rgba(var(--sidebar-text-rgb), 0.08);
|
||||||
|
color: rgba(var(--sidebar-text-rgb), 0.72);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(var(--sidebar-text-rgb), 0.16);
|
||||||
|
color: var(--sidebar-text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,19 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
|
|
||||||
import {useIntl} from 'react-intl'
|
import {FormattedMessage, useIntl} from 'react-intl'
|
||||||
|
|
||||||
|
import MenuWrapper from '../../widgets/menuWrapper'
|
||||||
|
import CompassIcon from '../../widgets/icons/compassIcon'
|
||||||
|
import Menu from '../../widgets/menu'
|
||||||
import Search from '../../widgets/icons/search'
|
import Search from '../../widgets/icons/search'
|
||||||
|
import CreateCategory from '../createCategory/createCategory'
|
||||||
import {useAppSelector} from '../../store/hooks'
|
import {useAppSelector} from '../../store/hooks'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getOnboardingTourCategory,
|
getOnboardingTourCategory,
|
||||||
getOnboardingTourStep,
|
getOnboardingTourStep,
|
||||||
} from '../../store/users'
|
} from '../../store/users'
|
||||||
|
|
||||||
import {getCurrentCard} from '../../store/cards'
|
import {getCurrentCard} from '../../store/cards'
|
||||||
|
|
||||||
import './boardsSwitcher.scss'
|
import './boardsSwitcher.scss'
|
||||||
@ -26,7 +28,8 @@ import IconButton from '../../widgets/buttons/iconButton'
|
|||||||
import SearchForBoardsTourStep from '../../components/onboardingTour/searchForBoards/searchForBoards'
|
import SearchForBoardsTourStep from '../../components/onboardingTour/searchForBoards/searchForBoards'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBoardTemplateSelectorOpen?: () => void,
|
onBoardTemplateSelectorOpen: () => void,
|
||||||
|
userIsGuest?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BoardsSwitcher = (props: Props): JSX.Element => {
|
const BoardsSwitcher = (props: Props): JSX.Element => {
|
||||||
@ -34,11 +37,11 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
|
|||||||
|
|
||||||
const [showSwitcher, setShowSwitcher] = useState<boolean>(false)
|
const [showSwitcher, setShowSwitcher] = useState<boolean>(false)
|
||||||
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
|
const onboardingTourCategory = useAppSelector(getOnboardingTourCategory)
|
||||||
|
const [showCreateCategoryModal, setShowCreateCategoryModal] = useState(false)
|
||||||
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
|
const onboardingTourStep = useAppSelector(getOnboardingTourStep)
|
||||||
const currentCard = useAppSelector(getCurrentCard)
|
const currentCard = useAppSelector(getCurrentCard)
|
||||||
const noCardOpen = !currentCard
|
const noCardOpen = !currentCard
|
||||||
|
|
||||||
|
|
||||||
const shouldViewSearchForBoardsTour = noCardOpen &&
|
const shouldViewSearchForBoardsTour = noCardOpen &&
|
||||||
onboardingTourCategory === TOUR_SIDEBAR &&
|
onboardingTourCategory === TOUR_SIDEBAR &&
|
||||||
onboardingTourStep === SidebarTourSteps.SEARCH_FOR_BOARDS.toString()
|
onboardingTourStep === SidebarTourSteps.SEARCH_FOR_BOARDS.toString()
|
||||||
@ -67,6 +70,10 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCreateNewCategory = () => {
|
||||||
|
setShowCreateCategoryModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', handleQuickSwitchKeyPress)
|
document.addEventListener('keydown', handleQuickSwitchKeyPress)
|
||||||
document.addEventListener('keydown', handleEscKeyPress)
|
document.addEventListener('keydown', handleEscKeyPress)
|
||||||
@ -93,20 +100,54 @@ const BoardsSwitcher = (props: Props): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
{shouldViewSearchForBoardsTour && <div><SearchForBoardsTourStep/></div>}
|
{shouldViewSearchForBoardsTour && <div><SearchForBoardsTourStep/></div>}
|
||||||
{
|
{
|
||||||
Utils.isFocalboardPlugin() &&
|
Utils.isFocalboardPlugin() && !props.userIsGuest &&
|
||||||
<IconButton
|
<MenuWrapper>
|
||||||
size='small'
|
<IconButton
|
||||||
inverted={true}
|
size='small'
|
||||||
className='add-board-icon'
|
inverted={true}
|
||||||
onClick={props.onBoardTemplateSelectorOpen}
|
className='add-board-icon'
|
||||||
icon={<AddIcon/>}
|
icon={<AddIcon/>}
|
||||||
/>
|
/>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Text
|
||||||
|
id='create-new-board-option'
|
||||||
|
icon={<CompassIcon icon='plus' />}
|
||||||
|
onClick={props.onBoardTemplateSelectorOpen}
|
||||||
|
name='Create new board'
|
||||||
|
/>
|
||||||
|
<Menu.Text
|
||||||
|
id='createNewCategory'
|
||||||
|
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||||
|
icon={
|
||||||
|
<CompassIcon
|
||||||
|
icon='folder-plus-outline'
|
||||||
|
className='CreateNewFolderIcon'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleCreateNewCategory}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</MenuWrapper>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
showSwitcher &&
|
showSwitcher &&
|
||||||
<BoardSwitcherDialog onClose={() => setShowSwitcher(false)} />
|
<BoardSwitcherDialog onClose={() => setShowSwitcher(false)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showCreateCategoryModal && (
|
||||||
|
<CreateCategory
|
||||||
|
onClose={() => setShowCreateCategoryModal(false)}
|
||||||
|
title={(
|
||||||
|
<FormattedMessage
|
||||||
|
id='SidebarCategories.CategoryMenu.CreateNew'
|
||||||
|
defaultMessage='Create New Category'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ exports[`component/BoardSwitcherDialog base case 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -10,10 +10,6 @@
|
|||||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -14,7 +14,9 @@ exports[`components/cardDetail/comment return comment 1`] = `
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="comment-username"
|
class="comment-username"
|
||||||
/>
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="octo-tooltip tooltip-top"
|
class="octo-tooltip tooltip-top"
|
||||||
data-tooltip="October 01, 2020, 12:00 AM"
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
@ -136,7 +138,9 @@ exports[`components/cardDetail/comment return comment and delete comment 1`] = `
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="comment-username"
|
class="comment-username"
|
||||||
/>
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="octo-tooltip tooltip-top"
|
class="octo-tooltip tooltip-top"
|
||||||
data-tooltip="October 01, 2020, 12:00 AM"
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
@ -258,7 +262,323 @@ exports[`components/cardDetail/comment return comment readonly 1`] = `
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="comment-username"
|
class="comment-username"
|
||||||
/>
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="octo-tooltip tooltip-top"
|
||||||
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-date"
|
||||||
|
>
|
||||||
|
a day ago
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="comment-text"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Test comment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/cardDetail/comment return guest comment 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Comment comment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-header"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="comment-avatar"
|
||||||
|
src="data:image/svg+xml"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="comment-username"
|
||||||
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="GuestBadge"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="GuestBadge__box"
|
||||||
|
>
|
||||||
|
Guest
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="octo-tooltip tooltip-top"
|
||||||
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-date"
|
||||||
|
>
|
||||||
|
a day ago
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="IconButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect left "
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Delete"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="comment-text"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Test comment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/cardDetail/comment return guest comment and delete comment 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Comment comment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-header"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="comment-avatar"
|
||||||
|
src="data:image/svg+xml"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="comment-username"
|
||||||
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="GuestBadge"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="GuestBadge__box"
|
||||||
|
>
|
||||||
|
Guest
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="octo-tooltip tooltip-top"
|
||||||
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-date"
|
||||||
|
>
|
||||||
|
a day ago
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="IconButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect left "
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Delete"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="comment-text"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Test comment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/cardDetail/comment return guest comment readonly 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Comment comment"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="comment-header"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="comment-avatar"
|
||||||
|
src="data:image/svg+xml"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="comment-username"
|
||||||
|
>
|
||||||
|
username_1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="GuestBadge"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="GuestBadge__box"
|
||||||
|
>
|
||||||
|
Guest
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="octo-tooltip tooltip-top"
|
class="octo-tooltip tooltip-top"
|
||||||
data-tooltip="October 01, 2020, 12:00 AM"
|
data-tooltip="October 01, 2020, 12:00 AM"
|
||||||
|
@ -68,9 +68,9 @@ describe('components/cardDetail/CardDetail', () => {
|
|||||||
const mockStore = configureStore([])
|
const mockStore = configureStore([])
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
users: {
|
users: {
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
current: {id: 'team-id'},
|
current: {id: 'team-id'},
|
||||||
@ -149,9 +149,9 @@ describe('components/cardDetail/CardDetail', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -211,9 +211,9 @@ describe('components/cardDetail/CardDetail', () => {
|
|||||||
focalboard_onboardingTourStep: '0',
|
focalboard_onboardingTourStep: '0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
current: {id: 'team-id'},
|
current: {id: 'team-id'},
|
||||||
@ -317,9 +317,9 @@ describe('components/cardDetail/CardDetail', () => {
|
|||||||
focalboard_onboardingTourStep: '1',
|
focalboard_onboardingTourStep: '1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
current: {id: 'team-id'},
|
current: {id: 'team-id'},
|
||||||
@ -421,9 +421,9 @@ describe('components/cardDetail/CardDetail', () => {
|
|||||||
focalboard_onboardingTourStep: '2',
|
focalboard_onboardingTourStep: '2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
current: {id: 'team-id'},
|
current: {id: 'team-id'},
|
||||||
|
@ -61,6 +61,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||||||
}
|
}
|
||||||
}, [card.title, title])
|
}, [card.title, title])
|
||||||
const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
|
const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
|
||||||
|
const canCommentBoardCards = useHasCurrentBoardPermissions([Permission.CommentBoardCards])
|
||||||
|
|
||||||
const saveTitleRef = useRef<() => void>(saveTitle)
|
const saveTitleRef = useRef<() => void>(saveTitle)
|
||||||
saveTitleRef.current = saveTitle
|
saveTitleRef.current = saveTitle
|
||||||
@ -207,7 +208,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
|
|||||||
comments={comments}
|
comments={comments}
|
||||||
boardId={card.boardId}
|
boardId={card.boardId}
|
||||||
cardId={card.id}
|
cardId={card.id}
|
||||||
readonly={props.readonly || !canEditBoardCards}
|
readonly={props.readonly || !canCommentBoardCards}
|
||||||
/>
|
/>
|
||||||
</Fragment>}
|
</Fragment>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,9 +32,7 @@ const userImageUrl = 'data:image/svg+xml'
|
|||||||
describe('components/cardDetail/comment', () => {
|
describe('components/cardDetail/comment', () => {
|
||||||
const state = {
|
const state = {
|
||||||
users: {
|
users: {
|
||||||
boardUsers: [
|
boardUsers: {[comment.modifiedBy]: {username: 'username_1'}},
|
||||||
{username: 'username_1'},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const store = mockStateStore([], state)
|
const store = mockStateStore([], state)
|
||||||
@ -101,4 +99,57 @@ describe('components/cardDetail/comment', () => {
|
|||||||
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
|
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
|
||||||
expect(mockedMutator.deleteBlock).toBeCalledWith(comment)
|
expect(mockedMutator.deleteBlock).toBeCalledWith(comment)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('return guest comment', () => {
|
||||||
|
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
|
||||||
|
const {container} = render(wrapIntl(
|
||||||
|
<ReduxProvider store={localStore}>
|
||||||
|
<Comment
|
||||||
|
comment={comment}
|
||||||
|
userId={comment.modifiedBy}
|
||||||
|
userImageUrl={userImageUrl}
|
||||||
|
readonly={false}
|
||||||
|
/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
))
|
||||||
|
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||||
|
userEvent.click(buttonElement)
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('return guest comment readonly', () => {
|
||||||
|
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
|
||||||
|
const {container} = render(wrapIntl(
|
||||||
|
<ReduxProvider store={localStore}>
|
||||||
|
<Comment
|
||||||
|
comment={comment}
|
||||||
|
userId={comment.modifiedBy}
|
||||||
|
userImageUrl={userImageUrl}
|
||||||
|
readonly={true}
|
||||||
|
/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
))
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('return guest comment and delete comment', () => {
|
||||||
|
const localStore = mockStateStore([], {users: {boardUsers: {[comment.modifiedBy]: {username: 'username_1', is_guest: true}}}})
|
||||||
|
const {container} = render(wrapIntl(
|
||||||
|
<ReduxProvider store={localStore}>
|
||||||
|
<Comment
|
||||||
|
comment={comment}
|
||||||
|
userId={comment.modifiedBy}
|
||||||
|
userImageUrl={userImageUrl}
|
||||||
|
readonly={false}
|
||||||
|
/>
|
||||||
|
</ReduxProvider>,
|
||||||
|
))
|
||||||
|
const buttonElement = screen.getByRole('button', {name: 'menuwrapper'})
|
||||||
|
userEvent.click(buttonElement)
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
const buttonDelete = screen.getByRole('button', {name: 'Delete'})
|
||||||
|
userEvent.click(buttonDelete)
|
||||||
|
expect(mockedMutator.deleteBlock).toBeCalledTimes(1)
|
||||||
|
expect(mockedMutator.deleteBlock).toBeCalledWith(comment)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -14,6 +14,7 @@ import MenuWrapper from '../../widgets/menuWrapper'
|
|||||||
import {getUser} from '../../store/users'
|
import {getUser} from '../../store/users'
|
||||||
import {useAppSelector} from '../../store/hooks'
|
import {useAppSelector} from '../../store/hooks'
|
||||||
import Tooltip from '../../widgets/tooltip'
|
import Tooltip from '../../widgets/tooltip'
|
||||||
|
import GuestBadge from '../../widgets/guestBadge'
|
||||||
|
|
||||||
import './comment.scss'
|
import './comment.scss'
|
||||||
|
|
||||||
@ -42,6 +43,8 @@ const Comment: FC<Props> = (props: Props) => {
|
|||||||
src={userImageUrl}
|
src={userImageUrl}
|
||||||
/>
|
/>
|
||||||
<div className='comment-username'>{user?.username}</div>
|
<div className='comment-username'>{user?.username}</div>
|
||||||
|
<GuestBadge show={user?.is_guest}/>
|
||||||
|
|
||||||
<Tooltip title={Utils.displayDateTime(date, intl)}>
|
<Tooltip title={Utils.displayDateTime(date, intl)}>
|
||||||
<div className='comment-date'>
|
<div className='comment-date'>
|
||||||
{Utils.relativeDisplayDateTime(date, intl)}
|
{Utils.relativeDisplayDateTime(date, intl)}
|
||||||
|
@ -48,15 +48,18 @@ describe('components/cardDetail/CommentsList', () => {
|
|||||||
const mockStore = configureStore([])
|
const mockStore = configureStore([])
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
users: {
|
users: {
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
boards: {
|
boards: {
|
||||||
boards: {
|
boards: {
|
||||||
board_id_1: {title: 'Board'},
|
board_id_1: {title: 'Board'},
|
||||||
},
|
},
|
||||||
current: 'board_id_1',
|
current: 'board_id_1',
|
||||||
|
myBoardMemberships: {
|
||||||
|
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cards: {
|
cards: {
|
||||||
@ -69,6 +72,9 @@ describe('components/cardDetail/CommentsList', () => {
|
|||||||
featureFlags: {},
|
featureFlags: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
teams: {
|
||||||
|
current: {id: 'team_id_1'},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
@ -105,10 +111,22 @@ describe('components/cardDetail/CommentsList', () => {
|
|||||||
const mockStore = configureStore([])
|
const mockStore = configureStore([])
|
||||||
const store = mockStore({
|
const store = mockStore({
|
||||||
users: {
|
users: {
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
|
boards: {
|
||||||
|
boards: {
|
||||||
|
board_id_1: {title: 'Board'},
|
||||||
|
},
|
||||||
|
current: 'board_id_1',
|
||||||
|
myBoardMemberships: {
|
||||||
|
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
current: {id: 'team_id_1'}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
|
@ -13,6 +13,8 @@ import {MarkdownEditor} from '../markdownEditor'
|
|||||||
|
|
||||||
import {IUser} from '../../user'
|
import {IUser} from '../../user'
|
||||||
import {getMe} from '../../store/users'
|
import {getMe} from '../../store/users'
|
||||||
|
import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
|
||||||
|
import {Permission} from '../../constants'
|
||||||
|
|
||||||
import AddCommentTourStep from '../onboardingTour/addComments/addComments'
|
import AddCommentTourStep from '../onboardingTour/addComments/addComments'
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ type Props = {
|
|||||||
const CommentsList = (props: Props) => {
|
const CommentsList = (props: Props) => {
|
||||||
const [newComment, setNewComment] = useState('')
|
const [newComment, setNewComment] = useState('')
|
||||||
const me = useAppSelector<IUser|null>(getMe)
|
const me = useAppSelector<IUser|null>(getMe)
|
||||||
|
const canDeleteOthersComments = useHasCurrentBoardPermissions([Permission.DeleteOthersComments])
|
||||||
|
|
||||||
const onSendClicked = () => {
|
const onSendClicked = () => {
|
||||||
const commentText = newComment
|
const commentText = newComment
|
||||||
@ -88,15 +91,20 @@ const CommentsList = (props: Props) => {
|
|||||||
{/* New comment */}
|
{/* New comment */}
|
||||||
{!props.readonly && newCommentComponent}
|
{!props.readonly && newCommentComponent}
|
||||||
|
|
||||||
{comments.slice(0).reverse().map((comment) => (
|
{comments.slice(0).reverse().map((comment) => {
|
||||||
<Comment
|
|
||||||
key={comment.id}
|
// Only modify _own_ comments, EXCEPT for Admins, which can delete _any_ comment
|
||||||
comment={comment}
|
// NOTE: editing comments will exist in the future (in addition to deleting)
|
||||||
userImageUrl={Utils.getProfilePicture(comment.modifiedBy)}
|
const canDeleteComment: boolean = canDeleteOthersComments || me?.id === comment.modifiedBy
|
||||||
userId={comment.modifiedBy}
|
return (
|
||||||
readonly={props.readonly}
|
<Comment
|
||||||
/>
|
key={comment.id}
|
||||||
))}
|
comment={comment}
|
||||||
|
userImageUrl={Utils.getProfilePicture(comment.modifiedBy)}
|
||||||
|
userId={comment.modifiedBy}
|
||||||
|
readonly={props.readonly || !canDeleteComment}
|
||||||
|
/>
|
||||||
|
)})}
|
||||||
|
|
||||||
{/* horizontal divider below comments */}
|
{/* horizontal divider below comments */}
|
||||||
{!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>}
|
{!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>}
|
||||||
|
@ -85,12 +85,9 @@ describe('components/centerPanel', () => {
|
|||||||
focalboard_onboardingTourStarted: false,
|
focalboard_onboardingTourStarted: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspaceUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
boardUsers: [
|
|
||||||
{username: 'username_1'},
|
|
||||||
],
|
|
||||||
blockSubscriptions: [],
|
blockSubscriptions: [],
|
||||||
},
|
},
|
||||||
teams: {
|
teams: {
|
||||||
|
@ -20,7 +20,7 @@ exports[`components/createCategory/CreateCategory base case should match snapsho
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -35,7 +35,9 @@ exports[`components/createCategory/CreateCategory base case should match snapsho
|
|||||||
<div
|
<div
|
||||||
class="CreateCategory"
|
class="CreateCategory"
|
||||||
>
|
>
|
||||||
<h3>
|
<h3
|
||||||
|
class="dialog-title"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
title
|
title
|
||||||
</span>
|
</span>
|
||||||
|
@ -4,20 +4,14 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
.dialog {
|
.dialog {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
height: 260px;
|
height: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
padding: 2px 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.CreateCategory {
|
.CreateCategory {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 40px 30px;
|
padding: 0 32px 24px;
|
||||||
gap: 12px;
|
gap: 24px;
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.inputWrapper {
|
.inputWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -38,18 +32,20 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
height: 48px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 2px solid rgba(var(--center-channel-color-rgb), 0.16);
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
|
background: var(--center-channel-bg);
|
||||||
|
color: var(--center-channel-color);
|
||||||
|
padding: 0 16px;
|
||||||
|
flex: 1;
|
||||||
|
transition: border 0.15s ease-in;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: var(--button-bg);
|
border-color: var(--button-bg);
|
||||||
|
box-shadow: inset 0 0 0 1px var(--button-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,20 +7,59 @@ import {render} from "@testing-library/react"
|
|||||||
|
|
||||||
import userEvent from "@testing-library/user-event"
|
import userEvent from "@testing-library/user-event"
|
||||||
|
|
||||||
import {wrapIntl} from "../../testUtils"
|
import thunk from "redux-thunk"
|
||||||
|
|
||||||
|
import {Provider as ReduxProvider} from 'react-redux'
|
||||||
|
|
||||||
|
import {mocked} from "jest-mock"
|
||||||
|
|
||||||
|
import {mockStateStore, wrapIntl} from "../../testUtils"
|
||||||
|
|
||||||
|
import {IUser} from "../../user"
|
||||||
|
|
||||||
|
import mutator from "../../mutator"
|
||||||
|
|
||||||
import CreateCategory from "./createCategory"
|
import CreateCategory from "./createCategory"
|
||||||
|
|
||||||
|
jest.mock('../../mutator')
|
||||||
|
const mockedMutator = mocked(mutator, true)
|
||||||
|
|
||||||
describe('components/createCategory/CreateCategory', () => {
|
describe('components/createCategory/CreateCategory', () => {
|
||||||
|
const me: IUser = {
|
||||||
|
id: 'user-id-1',
|
||||||
|
username: 'username_1',
|
||||||
|
email: '',
|
||||||
|
nickname: '',
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
props: {},
|
||||||
|
create_at: 0,
|
||||||
|
update_at: 0,
|
||||||
|
is_bot: false,
|
||||||
|
roles: 'system_user',
|
||||||
|
is_guest: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
teams: {
|
||||||
|
current: {id: 'team-id', title: 'Test Team'},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
me,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const store = mockStateStore([thunk], state)
|
||||||
|
|
||||||
it('base case should match snapshot', () => {
|
it('base case should match snapshot', () => {
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<CreateCategory
|
<ReduxProvider store={store}>
|
||||||
onClose={jest.fn()}
|
<CreateCategory
|
||||||
onCreate={jest.fn()}
|
onClose={jest.fn()}
|
||||||
title={
|
title={
|
||||||
<span>{'title'}</span>
|
<span>{'title'}</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
@ -30,13 +69,14 @@ describe('components/createCategory/CreateCategory', () => {
|
|||||||
it('should call onClose on being closed', () => {
|
it('should call onClose on being closed', () => {
|
||||||
const onCloseHandler = jest.fn()
|
const onCloseHandler = jest.fn()
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<CreateCategory
|
<ReduxProvider store={store}>
|
||||||
onClose={onCloseHandler}
|
<CreateCategory
|
||||||
onCreate={jest.fn()}
|
onClose={onCloseHandler}
|
||||||
title={
|
title={
|
||||||
<span>{'title'}</span>
|
<span>{'title'}</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
@ -52,35 +92,39 @@ describe('components/createCategory/CreateCategory', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should call onCreate on pressing enter', () => {
|
it('should call onCreate on pressing enter', () => {
|
||||||
const onCreateHandler = jest.fn()
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<CreateCategory
|
<ReduxProvider store={store}>
|
||||||
onClose={jest.fn()}
|
<CreateCategory
|
||||||
onCreate={onCreateHandler}
|
onClose={jest.fn()}
|
||||||
title={
|
title={
|
||||||
<span>{'title'}</span>
|
<span>{'title'}</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
const inputField = container.querySelector('.categoryNameInput')
|
const inputField = container.querySelector('.categoryNameInput')
|
||||||
expect(inputField).toBeTruthy()
|
expect(inputField).toBeTruthy()
|
||||||
userEvent.type(inputField as Element, 'category name{enter}')
|
userEvent.type(inputField as Element, 'category name{enter}')
|
||||||
expect(onCreateHandler).toBeCalledWith('category name')
|
expect(mockedMutator.createCategory).toBeCalledWith({
|
||||||
|
name: "category name",
|
||||||
|
teamID: "team-id",
|
||||||
|
userID: "user-id-1",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show initial value', () => {
|
it('should show initial value', () => {
|
||||||
const onCreateHandler = jest.fn()
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<CreateCategory
|
<ReduxProvider store={store}>
|
||||||
initialValue='Dwight prank ideas'
|
<CreateCategory
|
||||||
onClose={jest.fn()}
|
initialValue='Dwight prank ideas'
|
||||||
onCreate={onCreateHandler}
|
onClose={jest.fn()}
|
||||||
title={
|
title={
|
||||||
<span>{'title'}</span>
|
<span>{'title'}</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
@ -90,16 +134,16 @@ describe('components/createCategory/CreateCategory', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should clear input field on clicking clear icon', () => {
|
it('should clear input field on clicking clear icon', () => {
|
||||||
const onCreateHandler = jest.fn()
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<CreateCategory
|
<ReduxProvider store={store}>
|
||||||
initialValue='Dunder Mifflin'
|
<CreateCategory
|
||||||
onClose={jest.fn()}
|
initialValue='Dunder Mifflin'
|
||||||
onCreate={onCreateHandler}
|
onClose={jest.fn()}
|
||||||
title={
|
title={
|
||||||
<span>{'title'}</span>
|
<span>{'title'}</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</ReduxProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
|
@ -5,6 +5,17 @@ import React, {useState, KeyboardEvent} from 'react'
|
|||||||
|
|
||||||
import {useIntl} from 'react-intl'
|
import {useIntl} from 'react-intl'
|
||||||
|
|
||||||
|
import {IUser} from '../../user'
|
||||||
|
import {Category} from '../../store/sidebar'
|
||||||
|
import {getCurrentTeam} from '../../store/teams'
|
||||||
|
import mutator from '../../mutator'
|
||||||
|
import {useAppSelector} from '../../store/hooks'
|
||||||
|
import {
|
||||||
|
getMe,
|
||||||
|
} from '../../store/users'
|
||||||
|
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
import Dialog from '../dialog'
|
import Dialog from '../dialog'
|
||||||
import Button from '../../widgets/buttons/button'
|
import Button from '../../widgets/buttons/button'
|
||||||
|
|
||||||
@ -12,15 +23,18 @@ import './createCategory.scss'
|
|||||||
import CloseCircle from "../../widgets/icons/closeCircle"
|
import CloseCircle from "../../widgets/icons/closeCircle"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
boardCategoryId?: string
|
||||||
|
renameModal?: boolean
|
||||||
initialValue?: string
|
initialValue?: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onCreate: (name: string) => void
|
|
||||||
title: JSX.Element
|
title: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateCategory = (props: Props): JSX.Element => {
|
const CreateCategory = (props: Props): JSX.Element => {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const me = useAppSelector<IUser|null>(getMe)
|
||||||
|
const team = useAppSelector(getCurrentTeam)
|
||||||
|
const teamID = team?.id || ''
|
||||||
const placeholder = intl.formatMessage({id: 'Categories.CreateCategoryDialog.Placeholder', defaultMessage: 'Name your category' })
|
const placeholder = intl.formatMessage({id: 'Categories.CreateCategoryDialog.Placeholder', defaultMessage: 'Name your category' })
|
||||||
const cancelText = intl.formatMessage({id: 'Categories.CreateCategoryDialog.CancelText', defaultMessage: 'Cancel' })
|
const cancelText = intl.formatMessage({id: 'Categories.CreateCategoryDialog.CancelText', defaultMessage: 'Cancel' })
|
||||||
const createText = intl.formatMessage({id: 'Categories.CreateCategoryDialog.CreateText', defaultMessage: 'Create' })
|
const createText = intl.formatMessage({id: 'Categories.CreateCategoryDialog.CreateText', defaultMessage: 'Create' })
|
||||||
@ -30,17 +44,45 @@ const CreateCategory = (props: Props): JSX.Element => {
|
|||||||
|
|
||||||
const handleKeypress = (e: KeyboardEvent) => {
|
const handleKeypress = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
props.onCreate(name)
|
onCreate(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCreate = async (categoryName: string) => {
|
||||||
|
if (!me) {
|
||||||
|
Utils.logError('me not initialized')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.renameModal) {
|
||||||
|
const category: Category = {
|
||||||
|
name: categoryName,
|
||||||
|
id: props.boardCategoryId,
|
||||||
|
userID: me.id,
|
||||||
|
teamID,
|
||||||
|
} as Category
|
||||||
|
|
||||||
|
await mutator.updateCategory(category)
|
||||||
|
} else {
|
||||||
|
const category: Category = {
|
||||||
|
name: categoryName,
|
||||||
|
userID: me.id,
|
||||||
|
teamID,
|
||||||
|
} as Category
|
||||||
|
|
||||||
|
await mutator.createCategory(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
className='CreateCategoryModal'
|
className='CreateCategoryModal'
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
>
|
>
|
||||||
<div className='CreateCategory'>
|
<div className='CreateCategory'>
|
||||||
<h3>{props.title}</h3>
|
<h3 className='dialog-title'>{props.title}</h3>
|
||||||
<div className='inputWrapper'>
|
<div className='inputWrapper'>
|
||||||
<input
|
<input
|
||||||
className='categoryNameInput'
|
className='categoryNameInput'
|
||||||
@ -70,7 +112,7 @@ const CreateCategory = (props: Props): JSX.Element => {
|
|||||||
<Button
|
<Button
|
||||||
size={'medium'}
|
size={'medium'}
|
||||||
filled={Boolean(name.trim())}
|
filled={Boolean(name.trim())}
|
||||||
onClick={() => props.onCreate(name.trim())}
|
onClick={() => onCreate(name.trim())}
|
||||||
disabled={!(name.trim())}
|
disabled={!(name.trim())}
|
||||||
>
|
>
|
||||||
{props.initialValue ? updateText : createText}
|
{props.initialValue ? updateText : createText}
|
||||||
|
@ -10,6 +10,19 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog__close {
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
@include z-index(dialog-backdrop);
|
@include z-index(dialog-backdrop);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -26,14 +39,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
padding: 16px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: rgb(var(--center-channel-bg-rgb));
|
background-color: rgb(var(--center-channel-bg-rgb));
|
||||||
@ -55,6 +62,7 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 975px) {
|
@media screen and (max-width: 975px) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -63,26 +71,33 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
>* {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .banner {
|
>.banner {
|
||||||
background-color: rgba(230, 220, 192, 0.9);
|
background-color: rgba(230, 220, 192, 0.9);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .banner.error {
|
>.banner.error {
|
||||||
background-color: rgba(230, 192, 192, 0.9);
|
background-color: rgba(230, 192, 192, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
> .toolbar {
|
.IconButton {
|
||||||
display: flex;
|
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||||
flex-direction: row;
|
|
||||||
padding: 24px;
|
&:hover {
|
||||||
justify-content: space-between;
|
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||||
|
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||||
|
color: rgba(var(--button-bg-rgb), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar--right {
|
.toolbar--right {
|
||||||
@ -90,7 +105,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
>.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -98,12 +113,13 @@
|
|||||||
@media not screen and (max-width: 975px) {
|
@media not screen and (max-width: 975px) {
|
||||||
padding: 10px 126px;
|
padding: 10px 126px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 975px) {
|
@media screen and (max-width: 975px) {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content.fullwidth {
|
>.content.fullwidth {
|
||||||
padding-left: 78px;
|
padding-left: 78px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ const Dialog = (props: Props) => {
|
|||||||
}
|
}
|
||||||
isBackdropClickedRef.current = false
|
isBackdropClickedRef.current = false
|
||||||
props.onClose()
|
props.onClose()
|
||||||
|
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if(e.target === e.currentTarget){
|
if(e.target === e.currentTarget){
|
||||||
@ -58,10 +58,11 @@ const Dialog = (props: Props) => {
|
|||||||
className='dialog'
|
className='dialog'
|
||||||
>
|
>
|
||||||
<div className='toolbar'>
|
<div className='toolbar'>
|
||||||
{title && <h1 className='text-heading5 mt-2'>{title}</h1>}
|
{title && <h1 className='dialog-title'>{title}</h1>}
|
||||||
{
|
{
|
||||||
!props.hideCloseButton &&
|
!props.hideCloseButton &&
|
||||||
<IconButton
|
<IconButton
|
||||||
|
className='dialog__close'
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
icon={<CloseIcon/>}
|
icon={<CloseIcon/>}
|
||||||
title={closeDialogText}
|
title={closeDialogText}
|
||||||
|
@ -95,40 +95,41 @@ const GlobalHeaderSettingsMenu = (props: Props) => {
|
|||||||
isOn={randomIcons}
|
isOn={randomIcons}
|
||||||
onClick={async () => toggleRandomIcons()}
|
onClick={async () => toggleRandomIcons()}
|
||||||
/>
|
/>
|
||||||
<Menu.Text
|
{me?.is_guest !== true &&
|
||||||
id='product-tour'
|
<Menu.Text
|
||||||
className='product-tour'
|
id='product-tour'
|
||||||
name={intl.formatMessage({id: 'Sidebar.product-tour', defaultMessage: 'Product tour'})}
|
className='product-tour'
|
||||||
onClick={async () => {
|
name={intl.formatMessage({id: 'Sidebar.product-tour', defaultMessage: 'Product tour'})}
|
||||||
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.StartTour)
|
onClick={async () => {
|
||||||
|
TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.StartTour)
|
||||||
|
|
||||||
if (!me) {
|
if (!me) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!currentTeam) {
|
if (!currentTeam) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const patch: UserConfigPatch = {
|
const patch: UserConfigPatch = {
|
||||||
updatedFields: {
|
updatedFields: {
|
||||||
[UserPropPrefix + 'onboardingTourStarted']: '1',
|
[UserPropPrefix + 'onboardingTourStarted']: '1',
|
||||||
[UserPropPrefix + 'onboardingTourStep']: '0',
|
[UserPropPrefix + 'onboardingTourStep']: '0',
|
||||||
[UserPropPrefix + 'tourCategory']: 'onboarding',
|
[UserPropPrefix + 'tourCategory']: 'onboarding',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
const patchedProps = await octoClient.patchUserConfig(me.id, patch)
|
||||||
if (patchedProps) {
|
if (patchedProps) {
|
||||||
await dispatch(patchProps(patchedProps))
|
await dispatch(patchProps(patchedProps))
|
||||||
}
|
}
|
||||||
|
|
||||||
const onboardingData = await octoClient.prepareOnboarding(currentTeam.id)
|
const onboardingData = await octoClient.prepareOnboarding(currentTeam.id)
|
||||||
|
|
||||||
const newPath = `/team/${onboardingData?.teamID}/${onboardingData?.boardID}`
|
const newPath = `/team/${onboardingData?.teamID}/${onboardingData?.boardID}`
|
||||||
|
|
||||||
props.history.push(newPath)
|
props.history.push(newPath)
|
||||||
}}
|
}}
|
||||||
/>
|
/>}
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
28
webapp/src/components/guestNoBoards.scss
Normal file
28
webapp/src/components/guestNoBoards.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.GuestNoBoards {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 52px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 56px 0;
|
||||||
|
}
|
||||||
|
}
|
32
webapp/src/components/guestNoBoards.tsx
Normal file
32
webapp/src/components/guestNoBoards.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
|
import ErrorIllustration from '../svg/error-illustration'
|
||||||
|
|
||||||
|
import './guestNoBoards.scss'
|
||||||
|
|
||||||
|
const GuestNoBoards = () => {
|
||||||
|
return (
|
||||||
|
<div className='GuestNoBoards'>
|
||||||
|
<div>
|
||||||
|
<div className='title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='guest-no-board.title'
|
||||||
|
defaultMessage={'No boards yet'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='subtitle'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='guest-no-board.subtitle'
|
||||||
|
defaultMessage={'You don\'t have access to any board in this team yet, please wait until somebody adds you to any board.'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ErrorIllustration/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(GuestNoBoards)
|
@ -2,6 +2,9 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React, {ReactElement} from 'react'
|
import React, {ReactElement} from 'react'
|
||||||
import {EntryComponentProps} from '@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry'
|
import {EntryComponentProps} from '@draft-js-plugins/mention/lib/MentionSuggestions/Entry/Entry'
|
||||||
|
|
||||||
|
import GuestBadge from '../../../widgets/guestBadge'
|
||||||
|
|
||||||
import './entryComponent.scss'
|
import './entryComponent.scss'
|
||||||
|
|
||||||
const BotBadge = (window as any).Components?.BotBadge
|
const BotBadge = (window as any).Components?.BotBadge
|
||||||
@ -26,6 +29,7 @@ const Entry = (props: EntryComponentProps): ReactElement => {
|
|||||||
<div className={theme?.mentionSuggestionsEntryText}>
|
<div className={theme?.mentionSuggestionsEntryText}>
|
||||||
{mention.name}
|
{mention.name}
|
||||||
{BotBadge && <BotBadge show={mention.is_bot}/>}
|
{BotBadge && <BotBadge show={mention.is_bot}/>}
|
||||||
|
<GuestBadge show={mention.is_guest}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={theme?.mentionSuggestionsEntryText}>
|
<div className={theme?.mentionSuggestionsEntryText}>
|
||||||
{mention.displayName}
|
{mention.displayName}
|
||||||
|
@ -72,6 +72,7 @@ const MarkdownEditorInput = (props: Props): ReactElement => {
|
|||||||
name: user.username,
|
name: user.username,
|
||||||
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
|
avatar: `${imageURLForUser ? imageURLForUser(user.id) : ''}`,
|
||||||
is_bot: user.is_bot,
|
is_bot: user.is_bot,
|
||||||
|
is_guest: user.is_guest,
|
||||||
displayName: Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
displayName: Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}
|
||||||
))
|
))
|
||||||
setSuggestions(mentions)
|
setSuggestions(mentions)
|
||||||
|
@ -48,6 +48,7 @@ describe('components/messages/CloudMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -82,6 +83,7 @@ describe('components/messages/CloudMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -114,6 +116,7 @@ describe('components/messages/CloudMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -154,6 +157,7 @@ describe('components/messages/CloudMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours,
|
update_at: Date.now() - (1000 * 60 * 60 * 24), //24 hours,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -43,6 +43,7 @@ describe('components/messages/VersionMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -76,6 +77,7 @@ describe('components/messages/VersionMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -107,6 +109,7 @@ describe('components/messages/VersionMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@ -160,6 +163,7 @@ describe('components/messages/VersionMessage', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -8,18 +8,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
.dialog {
|
|
||||||
.toolbar {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
padding: 0;
|
|
||||||
position: absolute;
|
|
||||||
right: 18px;
|
|
||||||
top: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
.dialog {
|
.dialog {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -58,7 +46,7 @@
|
|||||||
padding: 0 32px;
|
padding: 0 32px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&.freesize {
|
&.freesize {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,683 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`src/components/shareBoard/userPermissionsRow should match snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="user-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="user-item__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ml-3"
|
||||||
|
>
|
||||||
|
<strong />
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
@username_1
|
||||||
|
</strong>
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
(You)
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="user-item__button"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-chevron-down CompassIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect left "
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Viewer"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Viewer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Commenter"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Commenter
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Editor"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Admin"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="CheckIcon Icon"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polyline
|
||||||
|
points="20,60 40,80 80,40"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MenuOption MenuSeparator menu-separator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Remove member"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Remove member
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in plugin mode 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="user-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="user-item__content"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="user-item__img"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="ml-3"
|
||||||
|
>
|
||||||
|
<strong />
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
@username_1
|
||||||
|
</strong>
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
(You)
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="user-item__button"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-chevron-down CompassIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect left "
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Viewer"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Viewer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Commenter"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Commenter
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Editor"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Admin"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="CheckIcon Icon"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polyline
|
||||||
|
points="20,60 40,80 80,40"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MenuOption MenuSeparator menu-separator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Remove member"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Remove member
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in template 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="user-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="user-item__content"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="user-item__img"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="ml-3"
|
||||||
|
>
|
||||||
|
<strong />
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
@username_1
|
||||||
|
</strong>
|
||||||
|
<strong
|
||||||
|
class="ml-2 text-light"
|
||||||
|
>
|
||||||
|
(You)
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="user-item__button"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-chevron-down CompassIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect left "
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Viewer"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Viewer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Editor"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Admin"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex menu-option__check"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="CheckIcon Icon"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polyline
|
||||||
|
points="20,60 40,80 80,40"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Admin
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MenuOption MenuSeparator menu-separator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Remove member"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Remove member
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -250,4 +250,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ShareBoard-user-selector-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,7 @@ const me: IUser = {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +132,7 @@ describe('src/components/shareBoard/shareBoard', () => {
|
|||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
me,
|
me,
|
||||||
boardUsers: [me],
|
boardUsers: {[me.id]: me},
|
||||||
blockSubscriptions: [],
|
blockSubscriptions: [],
|
||||||
},
|
},
|
||||||
boards: {
|
boards: {
|
||||||
|
@ -31,6 +31,7 @@ import Switch from '../../widgets/switch'
|
|||||||
import Button from '../../widgets/buttons/button'
|
import Button from '../../widgets/buttons/button'
|
||||||
import {sendFlashMessage} from '../flashMessages'
|
import {sendFlashMessage} from '../flashMessages'
|
||||||
import {Permission} from '../../constants'
|
import {Permission} from '../../constants'
|
||||||
|
import GuestBadge from '../../widgets/guestBadge'
|
||||||
|
|
||||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
||||||
|
|
||||||
@ -299,6 +300,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
|||||||
<div className='ml-3'>
|
<div className='ml-3'>
|
||||||
<strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong>
|
<strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong>
|
||||||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||||
|
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -358,6 +360,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
|||||||
value={selectedUser}
|
value={selectedUser}
|
||||||
className={'userSearchInput'}
|
className={'userSearchInput'}
|
||||||
cacheOptions={true}
|
cacheOptions={true}
|
||||||
|
filterOption={(o) => !members[o.value]}
|
||||||
loadOptions={async (inputValue: string) => {
|
loadOptions={async (inputValue: string) => {
|
||||||
const result = []
|
const result = []
|
||||||
if (Utils.isFocalboardPlugin()) {
|
if (Utils.isFocalboardPlugin()) {
|
||||||
|
@ -41,6 +41,7 @@ describe('src/components/shareBoard/teamPermissionsRow', () => {
|
|||||||
create_at: 0,
|
create_at: 0,
|
||||||
update_at: 0,
|
update_at: 0,
|
||||||
is_bot: false,
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
roles: 'system_user',
|
roles: 'system_user',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
173
webapp/src/components/shareBoard/userPermissionsRow.test.tsx
Normal file
173
webapp/src/components/shareBoard/userPermissionsRow.test.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import {act, render} from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import {Provider as ReduxProvider} from 'react-redux'
|
||||||
|
import thunk from 'redux-thunk'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import {MemoryRouter} from 'react-router'
|
||||||
|
import {mocked} from 'jest-mock'
|
||||||
|
|
||||||
|
import {BoardMember} from '../../blocks/board'
|
||||||
|
|
||||||
|
import {IUser} from '../../user'
|
||||||
|
import {TestBlockFactory} from '../../test/testBlockFactory'
|
||||||
|
import {mockStateStore, wrapDNDIntl} from '../../testUtils'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
|
import UserPermissionsRow from './userPermissionsRow'
|
||||||
|
|
||||||
|
jest.useFakeTimers()
|
||||||
|
|
||||||
|
const boardId = '1'
|
||||||
|
|
||||||
|
jest.mock('../../utils')
|
||||||
|
|
||||||
|
const mockedUtils = mocked(Utils, true)
|
||||||
|
|
||||||
|
const board = TestBlockFactory.createBoard()
|
||||||
|
board.id = boardId
|
||||||
|
board.teamId = 'team-id'
|
||||||
|
board.channelId = 'channel_1'
|
||||||
|
|
||||||
|
describe('src/components/shareBoard/userPermissionsRow', () => {
|
||||||
|
const me: IUser = {
|
||||||
|
id: 'user-id-1',
|
||||||
|
username: 'username_1',
|
||||||
|
email: '',
|
||||||
|
nickname: '',
|
||||||
|
firstname: '',
|
||||||
|
lastname: '',
|
||||||
|
props: {},
|
||||||
|
create_at: 0,
|
||||||
|
update_at: 0,
|
||||||
|
is_bot: false,
|
||||||
|
is_guest: false,
|
||||||
|
roles: 'system_user',
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
teams: {
|
||||||
|
current: {id: 'team-id', title: 'Test Team'},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
me,
|
||||||
|
boardUsers: [me],
|
||||||
|
blockSubscriptions: [],
|
||||||
|
},
|
||||||
|
boards: {
|
||||||
|
current: board.id,
|
||||||
|
boards: {
|
||||||
|
[board.id]: board,
|
||||||
|
},
|
||||||
|
templates: [],
|
||||||
|
membersInBoards: {
|
||||||
|
[board.id]: {},
|
||||||
|
},
|
||||||
|
myBoardMemberships: {
|
||||||
|
[board.id]: {userId: me.id, schemeAdmin: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should match snapshot', async () => {
|
||||||
|
let container: Element | undefined
|
||||||
|
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
|
||||||
|
const store = mockStateStore([thunk], state)
|
||||||
|
await act(async () => {
|
||||||
|
const result = render(
|
||||||
|
wrapDNDIntl(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<UserPermissionsRow
|
||||||
|
user={me}
|
||||||
|
isMe={true}
|
||||||
|
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||||
|
teammateNameDisplay={'test'}
|
||||||
|
onDeleteBoardMember={() => {}}
|
||||||
|
onUpdateBoardMember={() => {}}
|
||||||
|
/>
|
||||||
|
</ReduxProvider>),
|
||||||
|
{wrapper: MemoryRouter},
|
||||||
|
)
|
||||||
|
container = result.container
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonElement = container?.querySelector('.user-item__button')
|
||||||
|
expect(buttonElement).toBeDefined()
|
||||||
|
userEvent.click(buttonElement!)
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should match snapshot in plugin mode', async () => {
|
||||||
|
let container: Element | undefined
|
||||||
|
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||||
|
const store = mockStateStore([thunk], state)
|
||||||
|
await act(async () => {
|
||||||
|
const result = render(
|
||||||
|
wrapDNDIntl(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<UserPermissionsRow
|
||||||
|
user={me}
|
||||||
|
isMe={true}
|
||||||
|
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||||
|
teammateNameDisplay={'test'}
|
||||||
|
onDeleteBoardMember={() => {}}
|
||||||
|
onUpdateBoardMember={() => {}}/>
|
||||||
|
</ReduxProvider>),
|
||||||
|
{wrapper: MemoryRouter},
|
||||||
|
)
|
||||||
|
container = result.container
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonElement = container?.querySelector('.user-item__button')
|
||||||
|
expect(buttonElement).toBeDefined()
|
||||||
|
userEvent.click(buttonElement!)
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should match snapshot in template', async () => {
|
||||||
|
let container: Element | undefined
|
||||||
|
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
|
||||||
|
const testState = {
|
||||||
|
...state,
|
||||||
|
boards: {
|
||||||
|
...state.boards,
|
||||||
|
boards: {},
|
||||||
|
templates: {
|
||||||
|
[board.id]: {...board, isTemplate: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const store = mockStateStore([thunk], testState)
|
||||||
|
await act(async () => {
|
||||||
|
const result = render(
|
||||||
|
wrapDNDIntl(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<UserPermissionsRow
|
||||||
|
user={me}
|
||||||
|
isMe={true}
|
||||||
|
member={state.boards.myBoardMemberships[board.id] as BoardMember}
|
||||||
|
teammateNameDisplay={'test'}
|
||||||
|
onDeleteBoardMember={() => {}}
|
||||||
|
onUpdateBoardMember={() => {}}
|
||||||
|
/>
|
||||||
|
</ReduxProvider>),
|
||||||
|
{wrapper: MemoryRouter},
|
||||||
|
)
|
||||||
|
container = result.container
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonElement = container?.querySelector('.user-item__button')
|
||||||
|
expect(buttonElement).toBeDefined()
|
||||||
|
userEvent.click(buttonElement!)
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
@ -14,6 +14,9 @@ import {BoardMember} from '../../blocks/board'
|
|||||||
import {IUser} from '../../user'
|
import {IUser} from '../../user'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
import {Permission} from '../../constants'
|
import {Permission} from '../../constants'
|
||||||
|
import GuestBadge from '../../widgets/guestBadge'
|
||||||
|
import {useAppSelector} from '../../store/hooks'
|
||||||
|
import {getCurrentBoard} from '../../store/boards'
|
||||||
|
|
||||||
import BoardPermissionGate from '../permissions/boardPermissionGate'
|
import BoardPermissionGate from '../permissions/boardPermissionGate'
|
||||||
|
|
||||||
@ -28,6 +31,7 @@ type Props = {
|
|||||||
|
|
||||||
const UserPermissionsRow = (props: Props): JSX.Element => {
|
const UserPermissionsRow = (props: Props): JSX.Element => {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const board = useAppSelector(getCurrentBoard)
|
||||||
const {user, member, isMe, teammateNameDisplay} = props
|
const {user, member, isMe, teammateNameDisplay} = props
|
||||||
let currentRole = 'Viewer'
|
let currentRole = 'Viewer'
|
||||||
if (member.schemeAdmin) {
|
if (member.schemeAdmin) {
|
||||||
@ -51,6 +55,7 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
|||||||
<strong>{Utils.getUserDisplayName(user, teammateNameDisplay)}</strong>
|
<strong>{Utils.getUserDisplayName(user, teammateNameDisplay)}</strong>
|
||||||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||||
{isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>}
|
{isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>}
|
||||||
|
<GuestBadge show={user.is_guest}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -71,6 +76,14 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
|||||||
name={intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})}
|
name={intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})}
|
||||||
onClick={() => props.onUpdateBoardMember(member, 'Viewer')}
|
onClick={() => props.onUpdateBoardMember(member, 'Viewer')}
|
||||||
/>
|
/>
|
||||||
|
{!board.isTemplate &&
|
||||||
|
<Menu.Text
|
||||||
|
id='Commenter'
|
||||||
|
check={true}
|
||||||
|
icon={currentRole === 'Commenter' ? <CheckIcon/> : null}
|
||||||
|
name={intl.formatMessage({id: 'BoardMember.schemeCommenter', defaultMessage: 'Commenter'})}
|
||||||
|
onClick={() => props.onUpdateBoardMember(member, 'Commenter')}
|
||||||
|
/>}
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
id='Editor'
|
id='Editor'
|
||||||
check={true}
|
check={true}
|
||||||
@ -78,13 +91,14 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
|||||||
name={intl.formatMessage({id: 'BoardMember.schemeEditor', defaultMessage: 'Editor'})}
|
name={intl.formatMessage({id: 'BoardMember.schemeEditor', defaultMessage: 'Editor'})}
|
||||||
onClick={() => props.onUpdateBoardMember(member, 'Editor')}
|
onClick={() => props.onUpdateBoardMember(member, 'Editor')}
|
||||||
/>
|
/>
|
||||||
<Menu.Text
|
{user.is_guest !== true &&
|
||||||
id='Admin'
|
<Menu.Text
|
||||||
check={true}
|
id='Admin'
|
||||||
icon={currentRole === 'Admin' ? <CheckIcon/> : null}
|
check={true}
|
||||||
name={intl.formatMessage({id: 'BoardMember.schemeAdmin', defaultMessage: 'Admin'})}
|
icon={currentRole === 'Admin' ? <CheckIcon/> : null}
|
||||||
onClick={() => props.onUpdateBoardMember(member, 'Admin')}
|
name={intl.formatMessage({id: 'BoardMember.schemeAdmin', defaultMessage: 'Admin'})}
|
||||||
/>
|
onClick={() => props.onUpdateBoardMember(member, 'Admin')}
|
||||||
|
/>}
|
||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
id='Remove'
|
id='Remove'
|
||||||
|
@ -86,7 +86,7 @@ exports[`components/sidebarBoardItem sidebar board item 1`] = `
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
aria-label="menuwrapper"
|
aria-label="menuwrapper"
|
||||||
class="MenuWrapper x"
|
class="MenuWrapper menuOpen"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -97,6 +97,373 @@ exports[`components/sidebarBoardItem sidebar board item 1`] = `
|
|||||||
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect auto fixed"
|
||||||
|
style="top: 40px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MenuOption SubMenuOption menu-option boardMoveToCategorySubmenu"
|
||||||
|
id="moveBlock"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-folder-plus-outline CreateNewFolderIcon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Move To...
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="SubmenuTriangleIcon Icon"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="50,35 75,50 50,65"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Duplicate board"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-content-copy content-copy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Duplicate board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="New template from board"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-plus AddIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
New template from board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Hide board"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-close CloseIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Hide board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Delete board"
|
||||||
|
class="MenuOption TextOption menu-option text-danger"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Delete board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarBoardItem sidebar-view-item active"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="BoardIcon Icon"
|
||||||
|
fill="currentColor"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
opacity="0.8"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M4 4H20V20H4V4ZM2 4C2 2.89543 2.89543 2 4 2H20C21.1046 2 22 2.89543 22 4V20C22 21.1046 21.1046 22 20 22H4C2.89543 22 2 21.1046 2 20V4ZM8 6H6V12H8V6ZM11 6H13V16H11V6ZM18 6H16V9H18V6Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="octo-sidebar-title"
|
||||||
|
title="view title"
|
||||||
|
>
|
||||||
|
view title
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/sidebarBoardItem sidebar board item for guest 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="SidebarBoardItem subitem active"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="octo-sidebar-icon"
|
||||||
|
>
|
||||||
|
i
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="octo-sidebar-title"
|
||||||
|
title="board title"
|
||||||
|
>
|
||||||
|
board title
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="menuwrapper"
|
||||||
|
class="MenuWrapper menuOpen"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="IconButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-dots-horizontal OptionsIcon"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="Menu noselect auto fixed"
|
||||||
|
style="top: 40px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-options"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MenuOption SubMenuOption menu-option boardMoveToCategorySubmenu"
|
||||||
|
id="moveBlock"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-folder-plus-outline CreateNewFolderIcon"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Move To...
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="SubmenuTriangleIcon Icon"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<polygon
|
||||||
|
points="50,35 75,50 50,65"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Hide board"
|
||||||
|
class="MenuOption TextOption menu-option"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-close CloseIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Hide board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="Delete board"
|
||||||
|
class="MenuOption TextOption menu-option text-danger"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="CompassIcon icon-trash-can-outline DeleteIcon trash-can-outline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Delete board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-spacer hideOnWidescreen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="menu-options hideOnWidescreen"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Cancel"
|
||||||
|
class="MenuOption TextOption menu-option menu-cancel"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="menu-option__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="menu-name"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="noicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,11 +69,12 @@ describe('components/sidebarSidebar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const history = createMemoryHistory()
|
const history = createMemoryHistory()
|
||||||
|
const onBoardTemplateSelectorOpen = jest.fn()
|
||||||
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Sidebar/>
|
<Sidebar onBoardTemplateSelectorOpen={onBoardTemplateSelectorOpen}/>
|
||||||
</Router>
|
</Router>
|
||||||
</ReduxProvider>,
|
</ReduxProvider>,
|
||||||
)
|
)
|
||||||
@ -131,11 +132,12 @@ describe('components/sidebarSidebar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const history = createMemoryHistory()
|
const history = createMemoryHistory()
|
||||||
|
const onBoardTemplateSelectorOpen = jest.fn()
|
||||||
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Sidebar/>
|
<Sidebar onBoardTemplateSelectorOpen={onBoardTemplateSelectorOpen}/>
|
||||||
</Router>
|
</Router>
|
||||||
</ReduxProvider>,
|
</ReduxProvider>,
|
||||||
)
|
)
|
||||||
@ -192,11 +194,12 @@ describe('components/sidebarSidebar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const history = createMemoryHistory()
|
const history = createMemoryHistory()
|
||||||
|
const onBoardTemplateSelectorOpen = jest.fn()
|
||||||
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Sidebar/>
|
<Sidebar onBoardTemplateSelectorOpen={onBoardTemplateSelectorOpen}/>
|
||||||
</Router>
|
</Router>
|
||||||
</ReduxProvider>,
|
</ReduxProvider>,
|
||||||
)
|
)
|
||||||
@ -254,11 +257,12 @@ describe('components/sidebarSidebar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const history = createMemoryHistory()
|
const history = createMemoryHistory()
|
||||||
|
const onBoardTemplateSelectorOpen = jest.fn()
|
||||||
|
|
||||||
const component = wrapIntl(
|
const component = wrapIntl(
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Sidebar/>
|
<Sidebar onBoardTemplateSelectorOpen={onBoardTemplateSelectorOpen}/>
|
||||||
</Router>
|
</Router>
|
||||||
</ReduxProvider>,
|
</ReduxProvider>,
|
||||||
)
|
)
|
||||||
@ -305,7 +309,7 @@ describe('components/sidebarSidebar', () => {
|
|||||||
// const component = wrapIntl(
|
// const component = wrapIntl(
|
||||||
// <ReduxProvider store={store}>
|
// <ReduxProvider store={store}>
|
||||||
// <Router history={history}>
|
// <Router history={history}>
|
||||||
// <Sidebar/>
|
// <Sidebar onBoardTemplateSelectorOpen={onBoardTemplateSelectorOpen}/>
|
||||||
// </Router>
|
// </Router>
|
||||||
// </ReduxProvider>,
|
// </ReduxProvider>,
|
||||||
// )
|
// )
|
||||||
|
@ -11,6 +11,7 @@ import ShowSidebarIcon from '../../widgets/icons/showSidebar'
|
|||||||
import {getMySortedBoards} from '../../store/boards'
|
import {getMySortedBoards} from '../../store/boards'
|
||||||
import {useAppDispatch, useAppSelector} from '../../store/hooks'
|
import {useAppDispatch, useAppSelector} from '../../store/hooks'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
|
import {IUser} from "../../user"
|
||||||
|
|
||||||
import './sidebar.scss'
|
import './sidebar.scss'
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ import BoardsSwitcher from '../boardsSwitcher/boardsSwitcher'
|
|||||||
import wsClient, {WSClient} from '../../wsclient'
|
import wsClient, {WSClient} from '../../wsclient'
|
||||||
|
|
||||||
import {getCurrentTeam} from '../../store/teams'
|
import {getCurrentTeam} from '../../store/teams'
|
||||||
|
import {getMe} from '../../store/users'
|
||||||
|
|
||||||
import {Constants} from "../../constants"
|
import {Constants} from "../../constants"
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ import {addMissingItems} from './utils'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeBoardId?: string
|
activeBoardId?: string
|
||||||
onBoardTemplateSelectorOpen?: () => void
|
onBoardTemplateSelectorOpen: () => void
|
||||||
onBoardTemplateSelectorClose?: () => void
|
onBoardTemplateSelectorClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +62,7 @@ const Sidebar = (props: Props) => {
|
|||||||
const boards = useAppSelector(getMySortedBoards)
|
const boards = useAppSelector(getMySortedBoards)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const partialCategories = useAppSelector<Array<CategoryBoards>>(getSidebarCategories)
|
const partialCategories = useAppSelector<Array<CategoryBoards>>(getSidebarCategories)
|
||||||
|
const me = useAppSelector<IUser|null>(getMe)
|
||||||
const sidebarCategories = addMissingItems(partialCategories, boards)
|
const sidebarCategories = addMissingItems(partialCategories, boards)
|
||||||
const me = useAppSelector(getMe)
|
const me = useAppSelector(getMe)
|
||||||
const activeViewID = useAppSelector(getCurrentViewId)
|
const activeViewID = useAppSelector(getCurrentViewId)
|
||||||
@ -183,7 +186,10 @@ const Sidebar = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<BoardsSwitcher onBoardTemplateSelectorOpen={props.onBoardTemplateSelectorOpen}/>
|
<BoardsSwitcher
|
||||||
|
onBoardTemplateSelectorOpen={props.onBoardTemplateSelectorOpen}
|
||||||
|
userIsGuest={me?.is_guest}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='octo-sidebar-list'>
|
<div className='octo-sidebar-list'>
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ import {createMemoryHistory} from 'history'
|
|||||||
import {Router} from 'react-router-dom'
|
import {Router} from 'react-router-dom'
|
||||||
|
|
||||||
import {render} from '@testing-library/react'
|
import {render} from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
|
||||||
import {Provider as ReduxProvider} from 'react-redux'
|
import {Provider as ReduxProvider} from 'react-redux'
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ describe('components/sidebarBoardItem', () => {
|
|||||||
boards: {
|
boards: {
|
||||||
[board.id]: board,
|
[board.id]: board,
|
||||||
},
|
},
|
||||||
|
myBoardMemberships: {
|
||||||
|
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
current: view.id,
|
current: view.id,
|
||||||
@ -85,6 +89,9 @@ describe('components/sidebarBoardItem', () => {
|
|||||||
</ReduxProvider>,
|
</ReduxProvider>,
|
||||||
)
|
)
|
||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
|
const elementMenuWrapper = container.querySelector('.SidebarBoardItem div.MenuWrapper')
|
||||||
|
expect(elementMenuWrapper).not.toBeNull()
|
||||||
|
userEvent.click(elementMenuWrapper!)
|
||||||
expect(container).toMatchSnapshot()
|
expect(container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -111,4 +118,30 @@ describe('components/sidebarBoardItem', () => {
|
|||||||
const {container} = render(component)
|
const {container} = render(component)
|
||||||
expect(container).toMatchSnapshot()
|
expect(container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('sidebar board item for guest', () => {
|
||||||
|
const mockStore = configureStore([])
|
||||||
|
const store = mockStore({...state, users: { me: { is_guest: true }}})
|
||||||
|
|
||||||
|
const component = wrapIntl(
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<Router history={history}>
|
||||||
|
<SidebarBoardItem
|
||||||
|
categoryBoards={categoryBoards1}
|
||||||
|
board={board}
|
||||||
|
allCategories={allCategoryBoards}
|
||||||
|
isActive={true}
|
||||||
|
showBoard={jest.fn()}
|
||||||
|
showView={jest.fn()}
|
||||||
|
onDeleteRequest={jest.fn()}
|
||||||
|
/>
|
||||||
|
</Router>
|
||||||
|
</ReduxProvider>,
|
||||||
|
)
|
||||||
|
const {container} = render(component)
|
||||||
|
const elementMenuWrapper = container.querySelector('.SidebarBoardItem div.MenuWrapper')
|
||||||
|
expect(elementMenuWrapper).not.toBeNull()
|
||||||
|
userEvent.click(elementMenuWrapper!)
|
||||||
|
expect(container).toMatchSnapshot()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -235,20 +235,6 @@ const SidebarBoardItem = (props: Props) => {
|
|||||||
position='auto'
|
position='auto'
|
||||||
parentRef={boardItemRef}
|
parentRef={boardItemRef}
|
||||||
>
|
>
|
||||||
<BoardPermissionGate
|
|
||||||
boardId={board.id}
|
|
||||||
permissions={[Permission.DeleteBoard]}
|
|
||||||
>
|
|
||||||
<Menu.Text
|
|
||||||
key={`deleteBlock-${board.id}`}
|
|
||||||
id='deleteBlock'
|
|
||||||
name={intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'})}
|
|
||||||
icon={<DeleteIcon/>}
|
|
||||||
onClick={() => {
|
|
||||||
props.onDeleteRequest(board)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</BoardPermissionGate>
|
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
key={`moveBlock-${board.id}`}
|
key={`moveBlock-${board.id}`}
|
||||||
id='moveBlock'
|
id='moveBlock'
|
||||||
@ -259,24 +245,41 @@ const SidebarBoardItem = (props: Props) => {
|
|||||||
>
|
>
|
||||||
{generateMoveToCategoryOptions(board.id)}
|
{generateMoveToCategoryOptions(board.id)}
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.Text
|
{!me?.is_guest &&
|
||||||
id='duplicateBoard'
|
<Menu.Text
|
||||||
name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate board'})}
|
id='duplicateBoard'
|
||||||
icon={<DuplicateIcon/>}
|
name={intl.formatMessage({id: 'Sidebar.duplicate-board', defaultMessage: 'Duplicate board'})}
|
||||||
onClick={() => handleDuplicateBoard(board.isTemplate)}
|
icon={<DuplicateIcon/>}
|
||||||
/>
|
onClick={() => handleDuplicateBoard(board.isTemplate)}
|
||||||
<Menu.Text
|
/>}
|
||||||
id='templateFromBoard'
|
{!me?.is_guest &&
|
||||||
name={intl.formatMessage({id: 'Sidebar.template-from-board', defaultMessage: 'New template from board'})}
|
<Menu.Text
|
||||||
icon={<AddIcon/>}
|
id='templateFromBoard'
|
||||||
onClick={() => handleDuplicateBoard(true)}
|
name={intl.formatMessage({id: 'Sidebar.template-from-board', defaultMessage: 'New template from board'})}
|
||||||
/>
|
icon={<AddIcon/>}
|
||||||
|
onClick={() => handleDuplicateBoard(true)}
|
||||||
|
/>}
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
id='hideBoard'
|
id='hideBoard'
|
||||||
name={intl.formatMessage({id: 'HideBoard.MenuOption', defaultMessage: 'Hide board'})}
|
name={intl.formatMessage({id: 'HideBoard.MenuOption', defaultMessage: 'Hide board'})}
|
||||||
icon={<CloseIcon/>}
|
icon={<CloseIcon/>}
|
||||||
onClick={() => handleHideBoard()}
|
onClick={() => handleHideBoard()}
|
||||||
/>
|
/>
|
||||||
|
<BoardPermissionGate
|
||||||
|
boardId={board.id}
|
||||||
|
permissions={[Permission.DeleteBoard]}
|
||||||
|
>
|
||||||
|
<Menu.Text
|
||||||
|
key={`deleteBlock-${board.id}`}
|
||||||
|
id='deleteBlock'
|
||||||
|
className='text-danger'
|
||||||
|
name={intl.formatMessage({id: 'Sidebar.delete-board', defaultMessage: 'Delete board'})}
|
||||||
|
icon={<DeleteIcon/>}
|
||||||
|
onClick={() => {
|
||||||
|
props.onDeleteRequest(board)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BoardPermissionGate>
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import {Board} from '../../blocks/board'
|
|||||||
import mutator from '../../mutator'
|
import mutator from '../../mutator'
|
||||||
import IconButton from '../../widgets/buttons/iconButton'
|
import IconButton from '../../widgets/buttons/iconButton'
|
||||||
import DeleteIcon from '../../widgets/icons/delete'
|
import DeleteIcon from '../../widgets/icons/delete'
|
||||||
|
import CompassIcon from '../../widgets/icons/compassIcon'
|
||||||
import OptionsIcon from '../../widgets/icons/options'
|
import OptionsIcon from '../../widgets/icons/options'
|
||||||
import Menu from '../../widgets/menu'
|
import Menu from '../../widgets/menu'
|
||||||
import MenuWrapper from '../../widgets/menuWrapper'
|
import MenuWrapper from '../../widgets/menuWrapper'
|
||||||
@ -30,7 +31,6 @@ import {
|
|||||||
|
|
||||||
import {getCurrentCard} from '../../store/cards'
|
import {getCurrentCard} from '../../store/cards'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
import Update from '../../widgets/icons/update'
|
|
||||||
|
|
||||||
import { TOUR_SIDEBAR, SidebarTourSteps, TOUR_BOARD, FINISHED } from '../../components/onboardingTour/index'
|
import { TOUR_SIDEBAR, SidebarTourSteps, TOUR_BOARD, FINISHED } from '../../components/onboardingTour/index'
|
||||||
import telemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
import telemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
||||||
@ -252,6 +252,12 @@ const SidebarCategory = (props: Props) => {
|
|||||||
{
|
{
|
||||||
props.categoryBoards.id !== '' &&
|
props.categoryBoards.id !== '' &&
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
<Menu.Text
|
||||||
|
id='updateCategory'
|
||||||
|
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.Update', defaultMessage: 'Rename Category'})}
|
||||||
|
icon={<CompassIcon icon='pencil-outline'/>}
|
||||||
|
onClick={handleUpdateCategory}
|
||||||
|
/>
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
id='deleteCategory'
|
id='deleteCategory'
|
||||||
className='text-danger'
|
className='text-danger'
|
||||||
@ -259,11 +265,12 @@ const SidebarCategory = (props: Props) => {
|
|||||||
icon={<DeleteIcon/>}
|
icon={<DeleteIcon/>}
|
||||||
onClick={() => setShowDeleteCategoryDialog(true)}
|
onClick={() => setShowDeleteCategoryDialog(true)}
|
||||||
/>
|
/>
|
||||||
|
<Menu.Separator/>
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
id='updateCategory'
|
id='createNewCategory'
|
||||||
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.Update', defaultMessage: 'Rename Category'})}
|
name={intl.formatMessage({id: 'SidebarCategories.CategoryMenu.CreateNew', defaultMessage: 'Create New Category'})}
|
||||||
icon={<Update/>}
|
icon={<CreateNewFolder/>}
|
||||||
onClick={handleUpdateCategory}
|
onClick={handleCreateNewCategory}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
@ -323,21 +330,6 @@ const SidebarCategory = (props: Props) => {
|
|||||||
defaultMessage='Create New Category'
|
defaultMessage='Create New Category'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onCreate={async (name) => {
|
|
||||||
if (!me) {
|
|
||||||
Utils.logError('me not initialized')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const category: Category = {
|
|
||||||
name,
|
|
||||||
userID: me.id,
|
|
||||||
teamID,
|
|
||||||
} as Category
|
|
||||||
|
|
||||||
await mutator.createCategory(category)
|
|
||||||
setShowCreateCategoryModal(false)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -353,22 +345,6 @@ const SidebarCategory = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onClose={() => setShowUpdateCategoryModal(false)}
|
onClose={() => setShowUpdateCategoryModal(false)}
|
||||||
onCreate={async (name) => {
|
|
||||||
if (!me) {
|
|
||||||
Utils.logError('me not initialized')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const category: Category = {
|
|
||||||
name,
|
|
||||||
id: props.categoryBoards.id,
|
|
||||||
userID: me.id,
|
|
||||||
teamID,
|
|
||||||
} as Category
|
|
||||||
|
|
||||||
await mutator.updateCategory(category)
|
|
||||||
setShowUpdateCategoryModal(false)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -65,9 +65,9 @@ describe('components/viewHeader/viewHeaderGroupByMenu', () => {
|
|||||||
id: 'user-id-1',
|
id: 'user-id-1',
|
||||||
username: 'username_1',
|
username: 'username_1',
|
||||||
},
|
},
|
||||||
boardUsers: [
|
boardUsers: {
|
||||||
{username: 'username_1'},
|
'user-id-1': {username: 'username_1'},
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
boards: {
|
boards: {
|
||||||
current: board.id,
|
current: board.id,
|
||||||
|
@ -20,7 +20,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show notify upgrade button
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -96,7 +96,7 @@ exports[`components/viewLimitDialog/ViewLiimitDialog show upgrade button for sys
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -20,7 +20,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show notify admin confirmat
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -130,7 +130,7 @@ exports[`components/viewLimitDialog/ViewL]imitDialog show view limit dialog 1`]
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
class="IconButton size--medium"
|
class="IconButton dialog__close size--medium"
|
||||||
title="Close dialog"
|
title="Close dialog"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user