You've already forked focalboard
mirror of
https://github.com/mattermost/focalboard.git
synced 2025-07-15 23:54:29 +02:00
Merge branch 'main' into issue-2617
This commit is contained in:
@ -87,7 +87,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
||||||
|
|
||||||
// Member APIs
|
// Member APIs
|
||||||
@ -96,6 +96,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
|
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
|
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
|
||||||
apiv1.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
|
||||||
|
apiv1.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST")
|
||||||
|
|
||||||
// Sharing APIs
|
// Sharing APIs
|
||||||
apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
||||||
@ -137,8 +138,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
|||||||
apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}/blocks/{blockID}", a.sessionRequired(a.handleUpdateCategoryBlock)).Methods(http.MethodPost)
|
apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}/blocks/{blockID}", a.sessionRequired(a.handleUpdateCategoryBlock)).Methods(http.MethodPost)
|
||||||
|
|
||||||
// Get Files API
|
// Get Files API
|
||||||
files := r.PathPrefix("/files").Subrouter()
|
apiv1.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
|
||||||
files.HandleFunc("/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
|
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
apiv1.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
|
apiv1.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
|
||||||
@ -261,6 +261,8 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/Block"
|
// "$ref": "#/definitions/Block"
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -275,6 +277,12 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
|
|
||||||
|
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
||||||
|
if userID == "" && !hasValidReadToken {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
board, err := a.app.GetBoard(boardID)
|
board, err := a.app.GetBoard(boardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -285,9 +293,9 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.hasValidReadTokenForBoard(r, boardID) {
|
if !hasValidReadToken {
|
||||||
if board.IsTemplate {
|
if board.IsTemplate && board.Type == model.BoardTypeOpen {
|
||||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -474,6 +482,8 @@ func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
case errors.Is(err, app.ErrorCategoryDeleted):
|
case errors.Is(err, app.ErrorCategoryDeleted):
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err)
|
||||||
case errors.Is(err, app.ErrorCategoryPermissionDenied):
|
case errors.Is(err, app.ErrorCategoryPermissionDenied):
|
||||||
|
// TODO: The permissions should be handled as much as possible at
|
||||||
|
// the API level, this needs to be changed
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||||
default:
|
default:
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -505,9 +515,14 @@ func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
|
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, app.ErrorCategoryPermissionDenied) {
|
switch {
|
||||||
|
case errors.Is(err, app.ErrorInvalidCategory):
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||||
|
case errors.Is(err, app.ErrorCategoryPermissionDenied):
|
||||||
|
// TODO: The permissions should be handled as much as possible at
|
||||||
|
// the API level, this needs to be changed
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||||
} else {
|
default:
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -563,6 +578,7 @@ func (a *API) handleUpdateCategoryBlock(w http.ResponseWriter, r *http.Request)
|
|||||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||||
userID := session.UserID
|
userID := session.UserID
|
||||||
|
|
||||||
|
// TODO: Check the category and the team matches
|
||||||
err := a.app.AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID)
|
err := a.app.AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -755,7 +771,7 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// a user can update only own config
|
// a user can update only own config
|
||||||
if userID != session.UserID {
|
if userID != session.UserID {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -950,6 +966,8 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: block not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -993,7 +1011,7 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete undeleteBlock
|
// swagger:operation POST /api/v1/boards/{boardID}/blocks/{blockID}/undelete undeleteBlock
|
||||||
//
|
//
|
||||||
// Undeletes a block
|
// Undeletes a block
|
||||||
//
|
//
|
||||||
@ -1001,9 +1019,9 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
// parameters:
|
// parameters:
|
||||||
// - name: workspaceID
|
// - name: boardID
|
||||||
// in: path
|
// in: path
|
||||||
// description: Workspace ID
|
// description: Board ID
|
||||||
// required: true
|
// required: true
|
||||||
// type: string
|
// type: string
|
||||||
// - name: blockID
|
// - name: blockID
|
||||||
@ -1016,6 +1034,10 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/BlockPatch"
|
||||||
|
// '404':
|
||||||
|
// description: block not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -1027,19 +1049,56 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
blockID := vars["blockID"]
|
blockID := vars["blockID"]
|
||||||
|
boardID := vars["boardID"]
|
||||||
|
|
||||||
|
board, err := a.app.GetBoard(boardID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if board == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := a.app.GetLastBlockHistoryEntry(blockID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if block == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if board.ID != block.BoardID {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
auditRec.AddMeta("blockID", blockID)
|
auditRec.AddMeta("blockID", blockID)
|
||||||
|
|
||||||
err := a.app.UndeleteBlock(blockID, userID)
|
undeletedBlock, err := a.app.UndeleteBlock(blockID, userID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
undeletedBlockData, err := json.Marshal(undeletedBlock)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
|
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
|
||||||
jsonStringResponse(w, http.StatusOK, "{}")
|
jsonBytesResponse(w, http.StatusOK, undeletedBlockData)
|
||||||
|
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
@ -1074,6 +1133,8 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: block not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -1185,6 +1246,19 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i])
|
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, blockID := range patches.BlockIDs {
|
||||||
|
var block *model.Block
|
||||||
|
block, err = a.app.GetBlockByID(blockID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = a.app.PatchBlocks(teamID, patches, userID)
|
err = a.app.PatchBlocks(teamID, patches, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -1220,6 +1294,8 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/Sharing"
|
// "$ref": "#/definitions/Sharing"
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -1229,7 +1305,7 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
|||||||
boardID := vars["boardID"]
|
boardID := vars["boardID"]
|
||||||
|
|
||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1500,6 +1576,11 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
team, err := a.app.GetRootTeam()
|
team, err := a.app.GetRootTeam()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -1555,6 +1636,8 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: file not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -1566,6 +1649,11 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
|
|
||||||
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
||||||
|
if userID == "" && !hasValidReadToken {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
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
|
||||||
@ -1659,6 +1747,8 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/FileUploadResponse"
|
// "$ref": "#/definitions/FileUploadResponse"
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -1883,7 +1973,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
teamID := mux.Vars(r)["teamID"]
|
teamID := mux.Vars(r)["teamID"]
|
||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
|
|
||||||
if teamID != "0" && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1899,12 +1989,21 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results := []*model.Board{}
|
||||||
|
for _, board := range boards {
|
||||||
|
if board.Type == model.BoardTypeOpen {
|
||||||
|
results = append(results, board)
|
||||||
|
} else if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionViewBoard) {
|
||||||
|
results = append(results, board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a.logger.Debug("GetTemplates",
|
a.logger.Debug("GetTemplates",
|
||||||
mlog.String("teamID", teamID),
|
mlog.String("teamID", teamID),
|
||||||
mlog.Int("boardsCount", len(boards)),
|
mlog.Int("boardsCount", len(results)),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := json.Marshal(boards)
|
data, err := json.Marshal(results)
|
||||||
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
|
||||||
@ -1913,7 +2012,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
|||||||
// response
|
// response
|
||||||
jsonBytesResponse(w, http.StatusOK, data)
|
jsonBytesResponse(w, http.StatusOK, data)
|
||||||
|
|
||||||
auditRec.AddMeta("templatesCount", len(boards))
|
auditRec.AddMeta("templatesCount", len(results))
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2049,7 +2148,7 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// User can only delete subscriptions for themselves
|
// User can only delete subscriptions for themselves
|
||||||
if session.UserID != subscriberID {
|
if session.UserID != subscriberID {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2107,7 +2206,7 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// User can only get subscriptions for themselves (for now)
|
// User can only get subscriptions for themselves (for now)
|
||||||
if session.UserID != subscriberID {
|
if session.UserID != subscriberID {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2248,10 +2347,14 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
teamID := mux.Vars(r)["teamID"]
|
teamID := mux.Vars(r)["teamID"]
|
||||||
ctx := r.Context()
|
userID := getUserID(r)
|
||||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
|
||||||
|
|
||||||
teamID, boardID, err := a.app.PrepareOnboardingTour(session.UserID, teamID)
|
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
return
|
return
|
||||||
@ -2291,6 +2394,8 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/Board"
|
// "$ref": "#/definitions/Board"
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -2376,6 +2481,8 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// $ref: '#/definitions/Board'
|
// $ref: '#/definitions/Board'
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -2471,6 +2578,8 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -2479,6 +2588,17 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
boardID := mux.Vars(r)["boardID"]
|
boardID := mux.Vars(r)["boardID"]
|
||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
|
|
||||||
|
// Check if board exists
|
||||||
|
board, err := a.app.GetBoard(boardID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if board == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"})
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"})
|
||||||
return
|
return
|
||||||
@ -2520,6 +2640,8 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// $ref: '#/definitions/BoardsAndBlocks'
|
// $ref: '#/definitions/BoardsAndBlocks'
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -2531,11 +2653,6 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
asTemplate := query.Get("asTemplate")
|
asTemplate := query.Get("asTemplate")
|
||||||
toTeam := query.Get("toTeam")
|
toTeam := query.Get("toTeam")
|
||||||
|
|
||||||
if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||||
return
|
return
|
||||||
@ -2551,13 +2668,18 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if board.Type == model.BoardTypePrivate {
|
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if board.IsTemplate && board.Type == model.BoardTypeOpen {
|
||||||
|
if board.TeamID != model.GlobalTeamID && !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
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@ -2617,6 +2739,8 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/Block"
|
// "$ref": "#/definitions/Block"
|
||||||
|
// '404':
|
||||||
|
// description: board or block not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -2628,18 +2752,37 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
asTemplate := query.Get("asTemplate")
|
asTemplate := query.Get("asTemplate")
|
||||||
|
|
||||||
if userID == "" {
|
board, err := a.app.GetBoard(boardID)
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
board, err := a.app.GetBlockByID(blockID)
|
|
||||||
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
|
||||||
}
|
}
|
||||||
if board == nil {
|
if board == nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if userID == "" {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := a.app.GetBlockByID(blockID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if block == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if board.ID != block.BoardID {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2755,11 +2898,6 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
|
|||||||
// produces:
|
// produces:
|
||||||
// - application/json
|
// - application/json
|
||||||
// parameters:
|
// parameters:
|
||||||
// - name: boardID
|
|
||||||
// in: path
|
|
||||||
// description: Board ID
|
|
||||||
// required: true
|
|
||||||
// type: string
|
|
||||||
// - name: teamID
|
// - name: teamID
|
||||||
// in: path
|
// in: path
|
||||||
// description: Team ID
|
// description: Team ID
|
||||||
@ -2917,12 +3055,16 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
// description: success
|
// description: success
|
||||||
// schema:
|
// schema:
|
||||||
// $ref: '#/definitions/BoardMember'
|
// $ref: '#/definitions/BoardMember'
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/ErrorResponse"
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
|
||||||
boardID := mux.Vars(r)["boardID"]
|
boardID := mux.Vars(r)["boardID"]
|
||||||
|
userID := getUserID(r)
|
||||||
|
|
||||||
board, err := a.app.GetBoard(boardID)
|
board, err := a.app.GetBoard(boardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -2933,7 +3075,10 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID := getUserID(r)
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
requestBody, err := ioutil.ReadAll(r.Body)
|
requestBody, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2959,11 +3104,6 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
SchemeEditor: true,
|
SchemeEditor: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if board.Type == model.BoardTypePrivate && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "addMember", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "addMember", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
auditRec.AddMeta("boardID", boardID)
|
auditRec.AddMeta("boardID", boardID)
|
||||||
@ -3015,7 +3155,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// $ref: '#/definitions/BoardMember'
|
// $ref: '#/definitions/BoardMember'
|
||||||
// '404':
|
// '404':
|
||||||
// description: board not found
|
// description: board not found
|
||||||
// '503':
|
// '403':
|
||||||
// description: access denied
|
// description: access denied
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
@ -3067,7 +3207,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("AddMember",
|
a.logger.Debug("JoinBoard",
|
||||||
mlog.String("boardID", board.ID),
|
mlog.String("boardID", board.ID),
|
||||||
mlog.String("addedUserID", userID),
|
mlog.String("addedUserID", userID),
|
||||||
)
|
)
|
||||||
@ -3084,6 +3224,82 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// swagger:operation POST /boards/{boardID}/leave leaveBoard
|
||||||
|
//
|
||||||
|
// Remove your own membership from a board
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: boardID
|
||||||
|
// in: path
|
||||||
|
// description: Board ID
|
||||||
|
// required: true
|
||||||
|
// type: string
|
||||||
|
// security:
|
||||||
|
// - BearerAuth: []
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
|
// '403':
|
||||||
|
// description: access denied
|
||||||
|
// default:
|
||||||
|
// description: internal error
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/ErrorResponse"
|
||||||
|
|
||||||
|
userID := getUserID(r)
|
||||||
|
if userID == "" {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
boardID := mux.Vars(r)["boardID"]
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
board, err := a.app.GetBoard(boardID)
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if board == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRec := a.makeAuditRecord(r, "leaveBoard", audit.Fail)
|
||||||
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
|
auditRec.AddMeta("boardID", boardID)
|
||||||
|
auditRec.AddMeta("addedUserID", userID)
|
||||||
|
|
||||||
|
err = a.app.DeleteBoardMember(boardID, userID)
|
||||||
|
if errors.Is(err, app.ErrBoardMemberIsLastAdmin) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Debug("LeaveBoard",
|
||||||
|
mlog.String("boardID", board.ID),
|
||||||
|
mlog.String("addedUserID", userID),
|
||||||
|
)
|
||||||
|
|
||||||
|
jsonStringResponse(w, http.StatusOK, "{}")
|
||||||
|
|
||||||
|
auditRec.Success()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
|
||||||
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
|
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
|
||||||
//
|
//
|
||||||
@ -3207,6 +3423,8 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: success
|
// description: success
|
||||||
|
// '404':
|
||||||
|
// description: board not found
|
||||||
// default:
|
// default:
|
||||||
// description: internal error
|
// description: internal error
|
||||||
// schema:
|
// schema:
|
||||||
@ -3216,11 +3434,6 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
paramsUserID := mux.Vars(r)["userID"]
|
paramsUserID := mux.Vars(r)["userID"]
|
||||||
userID := getUserID(r)
|
userID := getUserID(r)
|
||||||
|
|
||||||
if paramsUserID != userID && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
board, err := a.app.GetBoard(boardID)
|
board, err := a.app.GetBoard(boardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||||
@ -3231,6 +3444,11 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
auditRec.AddMeta("boardID", boardID)
|
auditRec.AddMeta("boardID", boardID)
|
||||||
@ -3299,38 +3517,16 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, block := range newBab.Blocks {
|
if len(newBab.Boards) == 0 {
|
||||||
// Error checking
|
message := "at least one board is required"
|
||||||
if len(block.Type) < 1 {
|
|
||||||
message := fmt.Sprintf("missing type 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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if block.CreateAt < 1 {
|
|
||||||
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if block.UpdateAt < 1 {
|
|
||||||
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// permission check
|
|
||||||
createsPublicBoards := false
|
|
||||||
createsPrivateBoards := false
|
|
||||||
teamID := ""
|
teamID := ""
|
||||||
|
boardIDs := map[string]bool{}
|
||||||
for _, board := range newBab.Boards {
|
for _, board := range newBab.Boards {
|
||||||
if board.Type == model.BoardTypeOpen {
|
boardIDs[board.ID] = true
|
||||||
createsPublicBoards = true
|
|
||||||
}
|
|
||||||
if board.Type == model.BoardTypePrivate {
|
|
||||||
createsPrivateBoards = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if teamID == "" {
|
if teamID == "" {
|
||||||
teamID = board.TeamID
|
teamID = board.TeamID
|
||||||
@ -3350,6 +3546,38 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range newBab.Blocks {
|
||||||
|
// Error checking
|
||||||
|
if len(block.Type) < 1 {
|
||||||
|
message := fmt.Sprintf("missing type for block id %s", block.ID)
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.CreateAt < 1 {
|
||||||
|
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.UpdateAt < 1 {
|
||||||
|
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !boardIDs[block.BoardID] {
|
||||||
|
message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID)
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IDs of boards and blocks are used to confirm that they're
|
// IDs of boards and blocks are used to confirm that they're
|
||||||
// linked and then regenerated by the server
|
// linked and then regenerated by the server
|
||||||
newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger)
|
newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger)
|
||||||
@ -3358,15 +3586,6 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if createsPublicBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePublicChannel) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if createsPrivateBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePrivateChannel) {
|
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||||
auditRec.AddMeta("teamID", teamID)
|
auditRec.AddMeta("teamID", teamID)
|
||||||
@ -3503,6 +3722,11 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail)
|
||||||
@ -3575,7 +3799,9 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
// user must have permission to delete all the boards, and that
|
// user must have permission to delete all the boards, and that
|
||||||
// would include the permission to manage their blocks
|
// would include the permission to manage their blocks
|
||||||
teamID := ""
|
teamID := ""
|
||||||
|
boardIDMap := map[string]bool{}
|
||||||
for _, boardID := range dbab.Boards {
|
for _, boardID := range dbab.Boards {
|
||||||
|
boardIDMap[boardID] = true
|
||||||
// all boards in the request should belong to the same team
|
// all boards in the request should belong to the same team
|
||||||
board, err := a.app.GetBoard(boardID)
|
board, err := a.app.GetBoard(boardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -3601,6 +3827,28 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, blockID := range dbab.Blocks {
|
||||||
|
block, err2 := a.app.GetBlockByID(blockID)
|
||||||
|
if err2 != nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if block == nil {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := boardIDMap[block.BoardID]; !ok {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := dbab.IsValid(); err != nil {
|
if err := dbab.IsValid(); err != nil {
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||||
return
|
return
|
||||||
|
@ -46,6 +46,12 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
boardID := vars["boardID"]
|
boardID := vars["boardID"]
|
||||||
|
userID := getUserID(r)
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
|
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
|
||||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||||
@ -109,6 +115,11 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
teamID := vars["teamID"]
|
teamID := vars["teamID"]
|
||||||
|
|
||||||
@ -185,6 +196,11 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
teamID := vars["teamID"]
|
teamID := vars["teamID"]
|
||||||
|
|
||||||
|
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||||
|
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)
|
||||||
|
@ -170,6 +170,11 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||||
@ -235,6 +240,11 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||||
@ -288,6 +298,11 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||||
@ -390,6 +405,11 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.MattermostAuth {
|
||||||
|
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(a.singleUserToken) > 0 {
|
if len(a.singleUserToken) > 0 {
|
||||||
// Not permitted in single-user mode
|
// Not permitted in single-user mode
|
||||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||||
|
@ -282,35 +282,46 @@ func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
|
func (a *App) GetLastBlockHistoryEntry(blockID string) (*model.Block, error) {
|
||||||
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(blocks) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &blocks[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, error) {
|
||||||
|
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(blocks) == 0 {
|
if len(blocks) == 0 {
|
||||||
// undeleting non-existing block not considered an error
|
// undeleting non-existing block not considered an error
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.store.UndeleteBlock(blockID, modifiedBy)
|
err = a.store.UndeleteBlock(blockID, modifiedBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := a.store.GetBlock(blockID)
|
block, err := a.store.GetBlock(blockID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if block == nil {
|
if block == nil {
|
||||||
a.logger.Error("Error loading the block after undelete, not propagating through websockets or notifications")
|
a.logger.Error("Error loading the block after undelete, not propagating through websockets or notifications")
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
board, err := a.store.GetBoard(block.BoardID)
|
board, err := a.store.GetBoard(block.BoardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.blockChangeNotifier.Enqueue(func() error {
|
a.blockChangeNotifier.Enqueue(func() error {
|
||||||
@ -321,7 +332,7 @@ func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return block, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetBlockCountsByType() (map[string]int64, error) {
|
func (a *App) GetBlockCountsByType() (map[string]int64, error) {
|
||||||
|
@ -115,7 +115,7 @@ func TestUndeleteBlock(t *testing.T) {
|
|||||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
||||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
||||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
_, err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ func TestUndeleteBlock(t *testing.T) {
|
|||||||
).Return([]model.Block{block}, nil)
|
).Return([]model.Block{block}, nil)
|
||||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
_, err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||||
require.Error(t, err, "error")
|
require.Error(t, err, "error")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
|
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
|
||||||
ErrorCategoryDeleted = errors.New("category is deleted")
|
ErrorCategoryDeleted = errors.New("category is deleted")
|
||||||
|
ErrorInvalidCategory = errors.New("invalid category")
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
|
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
|
||||||
@ -86,6 +87,11 @@ func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category
|
|||||||
return nil, ErrorCategoryPermissionDenied
|
return nil, ErrorCategoryPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verify if category belongs to the team
|
||||||
|
if existingCategory.TeamID != teamID {
|
||||||
|
return nil, ErrorInvalidCategory
|
||||||
|
}
|
||||||
|
|
||||||
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
|
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -46,14 +46,14 @@ func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) getOnboardingBoardID() (string, error) {
|
func (a *App) getOnboardingBoardID() (string, error) {
|
||||||
boards, err := a.store.GetTemplateBoards(globalTeamID, "")
|
boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var onboardingBoardID string
|
var onboardingBoardID string
|
||||||
for _, block := range boards {
|
for _, block := range boards {
|
||||||
if block.Title == WelcomeBoardTitle && block.TeamID == globalTeamID {
|
if block.Title == WelcomeBoardTitle && block.TeamID == model.GlobalTeamID {
|
||||||
onboardingBoardID = block.ID
|
onboardingBoardID = block.ID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -81,5 +81,17 @@ func (a *App) createWelcomeBoard(userID, teamID string) (string, error) {
|
|||||||
return "", errCannotCreateBoard
|
return "", errCannotCreateBoard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// need variable for this to
|
||||||
|
// get reference for board patch
|
||||||
|
newType := model.BoardTypePrivate
|
||||||
|
|
||||||
|
patch := &model.BoardPatch{
|
||||||
|
Type: &newType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := a.PatchBoard(patch, bab.Boards[0].ID, userID); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return bab.Boards[0].ID, nil
|
return bab.Boards[0].ID, nil
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,17 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
|||||||
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
|
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
|
||||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
|
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}},
|
||||||
nil, nil)
|
nil, nil)
|
||||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
|
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2)
|
||||||
|
|
||||||
|
privateWelcomeBoard := model.Board{
|
||||||
|
ID: "board_id_1",
|
||||||
|
Title: "Welcome to Boards!",
|
||||||
|
TeamID: "0",
|
||||||
|
IsTemplate: true,
|
||||||
|
Type: model.BoardTypePrivate,
|
||||||
|
}
|
||||||
|
newType := model.BoardTypePrivate
|
||||||
|
th.Store.EXPECT().PatchBoard("board_id_1", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil)
|
||||||
|
|
||||||
userPropPatch := model.UserPropPatch{
|
userPropPatch := model.UserPropPatch{
|
||||||
UpdatedFields: map[string]string{
|
UpdatedFields: map[string]string{
|
||||||
@ -63,7 +73,16 @@ func TestCreateWelcomeBoard(t *testing.T) {
|
|||||||
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
|
th.Store.EXPECT().GetTemplateBoards("0", "").Return([]*model.Board{&welcomeBoard}, nil)
|
||||||
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).
|
th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).
|
||||||
Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil)
|
Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil)
|
||||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil)
|
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2)
|
||||||
|
privateWelcomeBoard := model.Board{
|
||||||
|
ID: "board_id_1",
|
||||||
|
Title: "Welcome to Boards!",
|
||||||
|
TeamID: "0",
|
||||||
|
IsTemplate: true,
|
||||||
|
Type: model.BoardTypePrivate,
|
||||||
|
}
|
||||||
|
newType := model.BoardTypePrivate
|
||||||
|
th.Store.EXPECT().PatchBoard("board_id_1", &model.BoardPatch{Type: &newType}, "user_id_1").Return(&privateWelcomeBoard, nil)
|
||||||
|
|
||||||
boardID, err := th.App.createWelcomeBoard(userID, teamID)
|
boardID, err := th.App.createWelcomeBoard(userID, teamID)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
defaultTemplateVersion = 2
|
defaultTemplateVersion = 2
|
||||||
globalTeamID = "0"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) InitTemplates() error {
|
func (a *App) InitTemplates() error {
|
||||||
@ -23,7 +22,7 @@ func (a *App) InitTemplates() error {
|
|||||||
|
|
||||||
// initializeTemplates imports default templates if the boards table is empty.
|
// initializeTemplates imports default templates if the boards table is empty.
|
||||||
func (a *App) initializeTemplates() (bool, error) {
|
func (a *App) initializeTemplates() (bool, error) {
|
||||||
boards, err := a.store.GetTemplateBoards(globalTeamID, "")
|
boards, err := a.store.GetTemplateBoards(model.GlobalTeamID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("cannot initialize templates: %w", err)
|
return false, fmt.Errorf("cannot initialize templates: %w", err)
|
||||||
}
|
}
|
||||||
@ -49,13 +48,13 @@ func (a *App) initializeTemplates() (bool, error) {
|
|||||||
r := bytes.NewReader(assets.DefaultTemplatesArchive)
|
r := bytes.NewReader(assets.DefaultTemplatesArchive)
|
||||||
|
|
||||||
opt := model.ImportArchiveOptions{
|
opt := model.ImportArchiveOptions{
|
||||||
TeamID: globalTeamID,
|
TeamID: model.GlobalTeamID,
|
||||||
ModifiedBy: "system",
|
ModifiedBy: "system",
|
||||||
BlockModifier: fixTemplateBlock,
|
BlockModifier: fixTemplateBlock,
|
||||||
BoardModifier: fixTemplateBoard,
|
BoardModifier: fixTemplateBoard,
|
||||||
}
|
}
|
||||||
if err = a.ImportArchive(r, opt); err != nil {
|
if err = a.ImportArchive(r, opt); err != nil {
|
||||||
return false, fmt.Errorf("cannot initialize global templates for team %s: %w", globalTeamID, err)
|
return false, fmt.Errorf("cannot initialize global templates for team %s: %w", model.GlobalTeamID, err)
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
func TestApp_initializeTemplates(t *testing.T) {
|
func TestApp_initializeTemplates(t *testing.T) {
|
||||||
board := &model.Board{
|
board := &model.Board{
|
||||||
ID: utils.NewID(utils.IDTypeBoard),
|
ID: utils.NewID(utils.IDTypeBoard),
|
||||||
TeamID: globalTeamID,
|
TeamID: model.GlobalTeamID,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
Title: "test board",
|
Title: "test board",
|
||||||
IsTemplate: true,
|
IsTemplate: true,
|
||||||
@ -43,7 +43,7 @@ func TestApp_initializeTemplates(t *testing.T) {
|
|||||||
th, tearDown := SetupTestHelper(t)
|
th, tearDown := SetupTestHelper(t)
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
|
|
||||||
th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{}, nil)
|
th.Store.EXPECT().GetTemplateBoards(model.GlobalTeamID, "").Return([]*model.Board{}, nil)
|
||||||
th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(nil)
|
th.Store.EXPECT().RemoveDefaultTemplates([]*model.Board{}).Return(nil)
|
||||||
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.Any(), gomock.Any()).AnyTimes().Return(boardsAndBlocks, nil)
|
th.Store.EXPECT().CreateBoardsAndBlocks(gomock.Any(), gomock.Any()).AnyTimes().Return(boardsAndBlocks, nil)
|
||||||
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
|
th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil)
|
||||||
@ -61,7 +61,7 @@ func TestApp_initializeTemplates(t *testing.T) {
|
|||||||
th, tearDown := SetupTestHelper(t)
|
th, tearDown := SetupTestHelper(t)
|
||||||
defer tearDown()
|
defer tearDown()
|
||||||
|
|
||||||
th.Store.EXPECT().GetTemplateBoards(globalTeamID, "").Return([]*model.Board{board}, nil)
|
th.Store.EXPECT().GetTemplateBoards(model.GlobalTeamID, "").Return([]*model.Board{board}, nil)
|
||||||
|
|
||||||
done, err := th.App.initializeTemplates()
|
done, err := th.App.initializeTemplates()
|
||||||
require.NoError(t, err, "initializeTemplates should not error")
|
require.NoError(t, err, "initializeTemplates should not error")
|
||||||
|
@ -180,6 +180,10 @@ func (c *Client) GetJoinBoardRoute(boardID string) string {
|
|||||||
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetLeaveBoardRoute(boardID string) string {
|
||||||
|
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) GetBlocksRoute(boardID string) string {
|
func (c *Client) GetBlocksRoute(boardID string) string {
|
||||||
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
|
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
|
||||||
}
|
}
|
||||||
@ -548,6 +552,16 @@ func (c *Client) JoinBoard(boardID string) (*model.BoardMember, *Response) {
|
|||||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) LeaveBoard(boardID string) (*model.BoardMember, *Response) {
|
||||||
|
r, err := c.DoAPIPost(c.GetLeaveBoardRoute(boardID), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
|
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
|
||||||
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
|
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -44,10 +44,12 @@ func TestGetBoards(t *testing.T) {
|
|||||||
teamID := "0"
|
teamID := "0"
|
||||||
otherTeamID := "other-team-id"
|
otherTeamID := "other-team-id"
|
||||||
user1 := th.GetUser1()
|
user1 := th.GetUser1()
|
||||||
|
user2 := th.GetUser2()
|
||||||
|
|
||||||
board1 := &model.Board{
|
board1 := &model.Board{
|
||||||
TeamID: teamID,
|
TeamID: teamID,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
|
Title: "Board 1",
|
||||||
}
|
}
|
||||||
rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true)
|
rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -56,14 +58,16 @@ func TestGetBoards(t *testing.T) {
|
|||||||
board2 := &model.Board{
|
board2 := &model.Board{
|
||||||
TeamID: teamID,
|
TeamID: teamID,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
|
Title: "Board 2",
|
||||||
}
|
}
|
||||||
rBoard2, err := th.Server.App().CreateBoard(board2, user1.ID, false)
|
rBoard2, err := th.Server.App().CreateBoard(board2, user2.ID, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, rBoard2)
|
require.NotNil(t, rBoard2)
|
||||||
|
|
||||||
board3 := &model.Board{
|
board3 := &model.Board{
|
||||||
TeamID: teamID,
|
TeamID: teamID,
|
||||||
Type: model.BoardTypePrivate,
|
Type: model.BoardTypePrivate,
|
||||||
|
Title: "Board 3",
|
||||||
}
|
}
|
||||||
rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true)
|
rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -72,35 +76,43 @@ func TestGetBoards(t *testing.T) {
|
|||||||
board4 := &model.Board{
|
board4 := &model.Board{
|
||||||
TeamID: teamID,
|
TeamID: teamID,
|
||||||
Type: model.BoardTypePrivate,
|
Type: model.BoardTypePrivate,
|
||||||
|
Title: "Board 4",
|
||||||
}
|
}
|
||||||
rBoard4, err := th.Server.App().CreateBoard(board4, user1.ID, false)
|
rBoard4, err := th.Server.App().CreateBoard(board4, user1.ID, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, rBoard4)
|
require.NotNil(t, rBoard4)
|
||||||
|
|
||||||
board5 := &model.Board{
|
board5 := &model.Board{
|
||||||
|
TeamID: teamID,
|
||||||
|
Type: model.BoardTypePrivate,
|
||||||
|
Title: "Board 5",
|
||||||
|
}
|
||||||
|
rBoard5, err := th.Server.App().CreateBoard(board5, user2.ID, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rBoard5)
|
||||||
|
|
||||||
|
board6 := &model.Board{
|
||||||
TeamID: otherTeamID,
|
TeamID: otherTeamID,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
}
|
}
|
||||||
rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true)
|
rBoard6, err := th.Server.App().CreateBoard(board6, user1.ID, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, rBoard5)
|
require.NotNil(t, rBoard6)
|
||||||
|
|
||||||
boards, resp := th.Client.GetBoardsForTeam(teamID)
|
boards, resp := th.Client.GetBoardsForTeam(teamID)
|
||||||
th.CheckOK(resp)
|
th.CheckOK(resp)
|
||||||
require.NotNil(t, boards)
|
require.NotNil(t, boards)
|
||||||
require.Len(t, boards, 2)
|
require.ElementsMatch(t, []*model.Board{
|
||||||
|
rBoard1,
|
||||||
boardIDs := []string{}
|
rBoard2,
|
||||||
for _, board := range boards {
|
rBoard3,
|
||||||
boardIDs = append(boardIDs, board.ID)
|
}, boards)
|
||||||
}
|
|
||||||
require.ElementsMatch(t, []string{rBoard1.ID, rBoard3.ID}, boardIDs)
|
|
||||||
|
|
||||||
boardsFromOtherTeam, resp := th.Client.GetBoardsForTeam(otherTeamID)
|
boardsFromOtherTeam, resp := th.Client.GetBoardsForTeam(otherTeamID)
|
||||||
th.CheckOK(resp)
|
th.CheckOK(resp)
|
||||||
require.NotNil(t, boardsFromOtherTeam)
|
require.NotNil(t, boardsFromOtherTeam)
|
||||||
require.Len(t, boardsFromOtherTeam, 1)
|
require.Len(t, boardsFromOtherTeam, 1)
|
||||||
require.Equal(t, rBoard5.ID, boardsFromOtherTeam[0].ID)
|
require.Equal(t, rBoard6.ID, boardsFromOtherTeam[0].ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,7 +827,7 @@ func TestDeleteBoard(t *testing.T) {
|
|||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
success, resp := th.Client.DeleteBoard("non-existing-board")
|
success, resp := th.Client.DeleteBoard("non-existing-board")
|
||||||
th.CheckForbidden(resp)
|
th.CheckNotFound(resp)
|
||||||
require.False(t, success)
|
require.False(t, success)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1023,15 +1035,26 @@ func TestAddMember(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
member, resp := th.Client2.AddMemberToBoard(newMember)
|
member, resp := th.Client2.AddMemberToBoard(newMember)
|
||||||
th.CheckOK(resp)
|
th.CheckForbidden(resp)
|
||||||
require.Equal(t, newMember.UserID, member.UserID)
|
require.Nil(t, member)
|
||||||
require.Equal(t, newMember.BoardID, member.BoardID)
|
|
||||||
require.Equal(t, newMember.SchemeAdmin, member.SchemeAdmin)
|
|
||||||
require.Equal(t, newMember.SchemeEditor, member.SchemeEditor)
|
|
||||||
require.False(t, member.SchemeCommenter)
|
|
||||||
require.False(t, member.SchemeViewer)
|
|
||||||
|
|
||||||
members, resp := th.Client.GetMembersForBoard(board.ID)
|
members, resp := th.Client2.GetMembersForBoard(board.ID)
|
||||||
|
th.CheckForbidden(resp)
|
||||||
|
require.Nil(t, members)
|
||||||
|
|
||||||
|
// Join board - will become an editor
|
||||||
|
member, resp = th.Client2.JoinBoard(board.ID)
|
||||||
|
th.CheckOK(resp)
|
||||||
|
require.NoError(t, resp.Error)
|
||||||
|
require.NotNil(t, member)
|
||||||
|
require.Equal(t, board.ID, member.BoardID)
|
||||||
|
require.Equal(t, th.GetUser2().ID, member.UserID)
|
||||||
|
|
||||||
|
member, resp = th.Client2.AddMemberToBoard(newMember)
|
||||||
|
th.CheckForbidden(resp)
|
||||||
|
require.Nil(t, member)
|
||||||
|
|
||||||
|
members, resp = th.Client2.GetMembersForBoard(board.ID)
|
||||||
th.CheckOK(resp)
|
th.CheckOK(resp)
|
||||||
require.Len(t, members, 2)
|
require.Len(t, members, 2)
|
||||||
})
|
})
|
||||||
@ -1370,13 +1393,14 @@ func TestDeleteMember(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, members, 2)
|
require.Len(t, members, 2)
|
||||||
|
|
||||||
|
// Should fail - must call leave to leave a board
|
||||||
success, resp := th.Client2.DeleteBoardMember(memberToDelete)
|
success, resp := th.Client2.DeleteBoardMember(memberToDelete)
|
||||||
th.CheckOK(resp)
|
th.CheckForbidden(resp)
|
||||||
require.True(t, success)
|
require.False(t, success)
|
||||||
|
|
||||||
members, err = th.Server.App().GetMembersForBoard(board.ID)
|
members, err = th.Server.App().GetMembersForBoard(board.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, members, 1)
|
require.Len(t, members, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
//nolint:dupl
|
//nolint:dupl
|
||||||
|
@ -517,7 +517,7 @@ func TestPatchBoardsAndBlocks(t *testing.T) {
|
|||||||
|
|
||||||
userID := th.GetUser1().ID
|
userID := th.GetUser1().ID
|
||||||
initialTitle := "initial title"
|
initialTitle := "initial title"
|
||||||
newTitle := "new title"
|
newTitle := "new patched title"
|
||||||
|
|
||||||
newBoard1 := &model.Board{
|
newBoard1 := &model.Board{
|
||||||
Title: initialTitle,
|
Title: initialTitle,
|
||||||
@ -580,7 +580,7 @@ func TestPatchBoardsAndBlocks(t *testing.T) {
|
|||||||
|
|
||||||
userID := th.GetUser1().ID
|
userID := th.GetUser1().ID
|
||||||
initialTitle := "initial title"
|
initialTitle := "initial title"
|
||||||
newTitle := "new title"
|
newTitle := "new other title"
|
||||||
|
|
||||||
newBoard1 := &model.Board{
|
newBoard1 := &model.Board{
|
||||||
Title: initialTitle,
|
Title: initialTitle,
|
||||||
|
@ -13,9 +13,11 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/server"
|
"github.com/mattermost/focalboard/server/server"
|
||||||
"github.com/mattermost/focalboard/server/services/config"
|
"github.com/mattermost/focalboard/server/services/config"
|
||||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||||
|
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||||
"github.com/mattermost/focalboard/server/services/store"
|
"github.com/mattermost/focalboard/server/services/store"
|
||||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||||
|
|
||||||
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -27,6 +29,16 @@ const (
|
|||||||
password = "Pa$$word"
|
password = "Pa$$word"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userAnon string = "anon"
|
||||||
|
userNoTeamMember string = "no-team-member"
|
||||||
|
userTeamMember string = "team-member"
|
||||||
|
userViewer string = "viewer"
|
||||||
|
userCommenter string = "commenter"
|
||||||
|
userEditor string = "editor"
|
||||||
|
userAdmin string = "admin"
|
||||||
|
)
|
||||||
|
|
||||||
type LicenseType int
|
type LicenseType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -42,6 +54,19 @@ type TestHelper struct {
|
|||||||
Client2 *client.Client
|
Client2 *client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FakePermissionPluginAPI struct{}
|
||||||
|
|
||||||
|
func (*FakePermissionPluginAPI) LogError(str string, params ...interface{}) {}
|
||||||
|
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
||||||
|
if userID == userNoTeamMember {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if teamID == "empty-team" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func getTestConfig() (*config.Configuration, error) {
|
func getTestConfig() (*config.Configuration, error) {
|
||||||
dbType, connectionString, err := sqlstore.PrepareNewTestDatabase()
|
dbType, connectionString, err := sqlstore.PrepareNewTestDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -135,6 +160,42 @@ func newTestServerWithLicense(singleUserToken string, licenseType LicenseType) *
|
|||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTestServerPluginMode() *server.Server {
|
||||||
|
cfg, err := getTestConfig()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cfg.AuthMode = "mattermost"
|
||||||
|
cfg.EnablePublicSharedBoards = true
|
||||||
|
|
||||||
|
logger, _ := mlog.NewLogger()
|
||||||
|
if err = logger.Configure("", cfg.LoggingCfgJSON, nil); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
innerStore, err := server.NewStore(cfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db := NewPluginTestStore(innerStore)
|
||||||
|
|
||||||
|
permissionsService := mmpermissions.New(db, &FakePermissionPluginAPI{})
|
||||||
|
|
||||||
|
params := server.Params{
|
||||||
|
Cfg: cfg,
|
||||||
|
DBStore: db,
|
||||||
|
Logger: logger,
|
||||||
|
PermissionsService: permissionsService,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv, err := server.New(params)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
||||||
sessionToken := "TESTTOKEN"
|
sessionToken := "TESTTOKEN"
|
||||||
th := &TestHelper{T: t}
|
th := &TestHelper{T: t}
|
||||||
@ -148,6 +209,13 @@ func SetupTestHelper(t *testing.T) *TestHelper {
|
|||||||
return SetupTestHelperWithLicense(t, LicenseNone)
|
return SetupTestHelperWithLicense(t, LicenseNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
|
||||||
|
th := &TestHelper{T: t}
|
||||||
|
th.Server = newTestServerPluginMode()
|
||||||
|
th.Start()
|
||||||
|
return th
|
||||||
|
}
|
||||||
|
|
||||||
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
||||||
th := &TestHelper{T: t}
|
th := &TestHelper{T: t}
|
||||||
th.Server = newTestServerWithLicense("", licenseType)
|
th.Server = newTestServerWithLicense("", licenseType)
|
||||||
|
2148
server/integrationtests/permissions_test.go
Normal file
2148
server/integrationtests/permissions_test.go
Normal file
File diff suppressed because it is too large
Load Diff
198
server/integrationtests/pluginteststore.go
Normal file
198
server/integrationtests/pluginteststore.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package integrationtests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattermost/focalboard/server/model"
|
||||||
|
"github.com/mattermost/focalboard/server/services/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errTestStore = errors.New("plugin test store error")
|
||||||
|
|
||||||
|
type PluginTestStore struct {
|
||||||
|
store.Store
|
||||||
|
users map[string]*model.User
|
||||||
|
testTeam *model.Team
|
||||||
|
otherTeam *model.Team
|
||||||
|
emptyTeam *model.Team
|
||||||
|
baseTeam *model.Team
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPluginTestStore(innerStore store.Store) *PluginTestStore {
|
||||||
|
return &PluginTestStore{
|
||||||
|
Store: innerStore,
|
||||||
|
users: map[string]*model.User{
|
||||||
|
"no-team-member": {
|
||||||
|
ID: "no-team-member",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "no-team-member",
|
||||||
|
Email: "no-team-member@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
"team-member": {
|
||||||
|
ID: "team-member",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "team-member",
|
||||||
|
Email: "team-member@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
"viewer": {
|
||||||
|
ID: "viewer",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "viewer",
|
||||||
|
Email: "viewer@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
"commenter": {
|
||||||
|
ID: "commenter",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "commenter",
|
||||||
|
Email: "commenter@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
ID: "editor",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "editor",
|
||||||
|
Email: "editor@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
ID: "admin",
|
||||||
|
Props: map[string]interface{}{},
|
||||||
|
Username: "admin",
|
||||||
|
Email: "admin@sample.com",
|
||||||
|
CreateAt: model.GetMillis(),
|
||||||
|
UpdateAt: model.GetMillis(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
|
||||||
|
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
|
||||||
|
emptyTeam: &model.Team{ID: "empty-team", Title: "Empty Team"},
|
||||||
|
baseTeam: &model.Team{ID: "0", Title: "Base Team"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetTeam(id string) (*model.Team, error) {
|
||||||
|
switch id {
|
||||||
|
case "0":
|
||||||
|
return s.baseTeam, nil
|
||||||
|
case "other-team":
|
||||||
|
return s.otherTeam, nil
|
||||||
|
case "test-team":
|
||||||
|
return s.testTeam, nil
|
||||||
|
case "empty-team":
|
||||||
|
return s.emptyTeam, nil
|
||||||
|
}
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetTeamsForUser(userID string) ([]*model.Team, error) {
|
||||||
|
switch userID {
|
||||||
|
case "no-team-member":
|
||||||
|
return []*model.Team{}, nil
|
||||||
|
case "team-member":
|
||||||
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
case "viewer":
|
||||||
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
case "commenter":
|
||||||
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
case "editor":
|
||||||
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
case "admin":
|
||||||
|
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||||
|
}
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetUserByID(userID string) (*model.User, error) {
|
||||||
|
user := s.users[userID]
|
||||||
|
if user == nil {
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetUserByEmail(email string) (*model.User, error) {
|
||||||
|
for _, user := range s.users {
|
||||||
|
if user.Email == email {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetUserByUsername(username string) (*model.User, error) {
|
||||||
|
for _, user := range s.users {
|
||||||
|
if user.Username == username {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) PatchUserProps(userID string, patch model.UserPropPatch) error {
|
||||||
|
user, err := s.GetUserByID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
props := user.Props
|
||||||
|
|
||||||
|
for _, key := range patch.DeletedFields {
|
||||||
|
delete(props, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range patch.UpdatedFields {
|
||||||
|
props[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Props = props
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||||
|
switch {
|
||||||
|
case teamID == s.testTeam.ID:
|
||||||
|
return []*model.User{
|
||||||
|
s.users["team-member"],
|
||||||
|
s.users["viewer"],
|
||||||
|
s.users["commenter"],
|
||||||
|
s.users["editor"],
|
||||||
|
s.users["admin"],
|
||||||
|
}, nil
|
||||||
|
case teamID == s.otherTeam.ID:
|
||||||
|
return []*model.User{
|
||||||
|
s.users["team-member"],
|
||||||
|
s.users["viewer"],
|
||||||
|
s.users["commenter"],
|
||||||
|
s.users["editor"],
|
||||||
|
s.users["admin"],
|
||||||
|
}, nil
|
||||||
|
case teamID == s.emptyTeam.ID:
|
||||||
|
return []*model.User{}, nil
|
||||||
|
}
|
||||||
|
return nil, errTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
||||||
|
users := []*model.User{}
|
||||||
|
teamUsers, err := s.GetUsersByTeam(teamID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range teamUsers {
|
||||||
|
if strings.Contains(user.Username, searchQuery) {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
SingleUser = "single-user"
|
SingleUser = "single-user"
|
||||||
|
GlobalTeamID = "0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User is a user
|
// User is a user
|
||||||
|
@ -11,15 +11,19 @@ import (
|
|||||||
"github.com/mattermost/focalboard/server/services/permissions"
|
"github.com/mattermost/focalboard/server/services/permissions"
|
||||||
|
|
||||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type APIInterface interface {
|
||||||
|
HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool
|
||||||
|
LogError(string, ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
store permissions.Store
|
store permissions.Store
|
||||||
api plugin.API
|
api APIInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(store permissions.Store, api plugin.API) *Service {
|
func New(store permissions.Store, api APIInterface) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
store: store,
|
store: store,
|
||||||
api: api,
|
api: api,
|
||||||
|
@ -241,11 +241,18 @@ func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, err
|
|||||||
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
||||||
query := s.getQueryBuilder(db).
|
query := s.getQueryBuilder(db).
|
||||||
Select(boardFields("b.")...).
|
Select(boardFields("b.")...).
|
||||||
|
Distinct().
|
||||||
From(s.tablePrefix + "boards as b").
|
From(s.tablePrefix + "boards as b").
|
||||||
Join(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{"bm.user_id": userID}).
|
Where(sq.Eq{"b.is_template": false}).
|
||||||
Where(sq.Eq{"b.is_template": false})
|
Where(sq.Or{
|
||||||
|
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||||
|
sq.And{
|
||||||
|
sq.Eq{"b.type": model.BoardTypePrivate},
|
||||||
|
sq.Eq{"bm.user_id": userID},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
rows, err := query.Query()
|
rows, err := query.Query()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
{{if .sqlite}}
|
||||||
|
ALTER TABLE {{.prefix}}categories DROP COLUMN channel_id VARCHAR(36);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .mysql}}
|
||||||
|
ALTER TABLE {{.prefix}}categories MODIFY COLUMN channel_id VARCHAR(36) NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}categories MODIFY COLUMN id INT NOT NULL UNIQUE AUTO_INCREMENT;
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks MODIFY COLUMN id INT NOT NULL UNIQUE AUTO_INCREMENT;
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .postgres}}
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN id DROP NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN id TYPE SERIAL;
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id DROP NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id TYPE SERIAL;
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id TYPE VARCHAR(32);
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id SET NOT NULL;
|
||||||
|
{{end}}
|
@ -0,0 +1,18 @@
|
|||||||
|
{{if .sqlite}}
|
||||||
|
ALTER TABLE {{.prefix}}categories ADD COLUMN channel_id VARCHAR(36);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .mysql}}
|
||||||
|
ALTER TABLE {{.prefix}}categories MODIFY COLUMN id VARCHAR(36) NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks MODIFY COLUMN id VARCHAR(36) NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}categories MODIFY COLUMN channel_id VARCHAR(36);
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .postgres}}
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN id TYPE VARCHAR(36);
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN id SET NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id TYPE VARCHAR(36);
|
||||||
|
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id SET NOT NULL;
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id TYPE VARCHAR(36);
|
||||||
|
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id DROP NOT NULL;
|
||||||
|
{{end}}
|
@ -120,7 +120,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
}
|
}
|
||||||
_, _, err := store.InsertBoardWithAdmin(board1, userID)
|
rBoard1, _, err := store.InsertBoardWithAdmin(board1, userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
board2 := &model.Board{
|
board2 := &model.Board{
|
||||||
@ -128,7 +128,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
Type: model.BoardTypePrivate,
|
Type: model.BoardTypePrivate,
|
||||||
}
|
}
|
||||||
_, _, err = store.InsertBoardWithAdmin(board2, userID)
|
rBoard2, _, err := store.InsertBoardWithAdmin(board2, userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
board3 := &model.Board{
|
board3 := &model.Board{
|
||||||
@ -136,7 +136,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
TeamID: teamID1,
|
TeamID: teamID1,
|
||||||
Type: model.BoardTypeOpen,
|
Type: model.BoardTypeOpen,
|
||||||
}
|
}
|
||||||
_, err = store.InsertBoard(board3, "other-user")
|
rBoard3, err := store.InsertBoard(board3, "other-user")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
board4 := &model.Board{
|
board4 := &model.Board{
|
||||||
@ -164,16 +164,14 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
|||||||
_, err = store.InsertBoard(board6, "other-user")
|
_, err = store.InsertBoard(board6, "other-user")
|
||||||
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", 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)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, boards, 2)
|
require.ElementsMatch(t, []*model.Board{
|
||||||
|
rBoard1,
|
||||||
boardIDs := []string{}
|
rBoard2,
|
||||||
for _, board := range boards {
|
rBoard3,
|
||||||
boardIDs = append(boardIDs, board.ID)
|
}, boards)
|
||||||
}
|
|
||||||
require.ElementsMatch(t, []string{board1.ID, board2.ID}, boardIDs)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -260,6 +260,32 @@ definitions:
|
|||||||
- schemeViewer
|
- schemeViewer
|
||||||
type: object
|
type: object
|
||||||
x-go-package: github.com/mattermost/focalboard/server/model
|
x-go-package: github.com/mattermost/focalboard/server/model
|
||||||
|
BoardMemberHistoryEntry:
|
||||||
|
description: BoardMemberHistoryEntry stores the information of the membership of a user on a board
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
description: The action that added this history entry (created or deleted)
|
||||||
|
type: string
|
||||||
|
x-go-name: Action
|
||||||
|
boardId:
|
||||||
|
description: The ID of the board
|
||||||
|
type: string
|
||||||
|
x-go-name: BoardID
|
||||||
|
insertAt:
|
||||||
|
description: The insertion time
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
x-go-name: InsertAt
|
||||||
|
userId:
|
||||||
|
description: The ID of the user
|
||||||
|
type: string
|
||||||
|
x-go-name: UserID
|
||||||
|
required:
|
||||||
|
- boardId
|
||||||
|
- userId
|
||||||
|
- insertAt
|
||||||
|
type: object
|
||||||
|
x-go-package: github.com/mattermost/focalboard/server/model
|
||||||
BoardMetadata:
|
BoardMetadata:
|
||||||
description: BoardMetadata contains metadata for a Board
|
description: BoardMetadata contains metadata for a Board
|
||||||
properties:
|
properties:
|
||||||
@ -882,6 +908,8 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: success
|
description: success
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -904,6 +932,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Board'
|
$ref: '#/definitions/Board'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -932,6 +962,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Board'
|
$ref: '#/definitions/Board'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -959,34 +991,6 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
summary: Exports an archive of all blocks for one boards.
|
summary: Exports an archive of all blocks for one boards.
|
||||||
/api/v1/boards/{boardID}/archive/import:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- multipart/form-data
|
|
||||||
operationId: archiveImport
|
|
||||||
parameters:
|
|
||||||
- description: Workspace ID
|
|
||||||
in: path
|
|
||||||
name: boardID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
- description: archive file to import
|
|
||||||
in: formData
|
|
||||||
name: file
|
|
||||||
required: true
|
|
||||||
type: file
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: success
|
|
||||||
default:
|
|
||||||
description: internal error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
summary: Import an archive of boards.
|
|
||||||
/api/v1/boards/{boardID}/blocks:
|
/api/v1/boards/{boardID}/blocks:
|
||||||
get:
|
get:
|
||||||
description: Returns blocks
|
description: Returns blocks
|
||||||
@ -1014,6 +1018,8 @@ paths:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/definitions/Block'
|
$ref: '#/definitions/Block'
|
||||||
type: array
|
type: array
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1102,6 +1108,8 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: success
|
description: success
|
||||||
|
"404":
|
||||||
|
description: block not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1133,6 +1141,8 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: success
|
description: success
|
||||||
|
"404":
|
||||||
|
description: block not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1163,6 +1173,8 @@ paths:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/definitions/Block'
|
$ref: '#/definitions/Block'
|
||||||
type: array
|
type: array
|
||||||
|
"404":
|
||||||
|
description: board or block not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1205,6 +1217,36 @@ paths:
|
|||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
|
/api/v1/boards/{boardID}/blocks/{blockID}/undelete:
|
||||||
|
post:
|
||||||
|
description: Undeletes a block
|
||||||
|
operationId: undeleteBlock
|
||||||
|
parameters:
|
||||||
|
- description: Board ID
|
||||||
|
in: path
|
||||||
|
name: boardID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: ID of block to undelete
|
||||||
|
in: path
|
||||||
|
name: blockID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: success
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/BlockPatch'
|
||||||
|
"404":
|
||||||
|
description: block not found
|
||||||
|
default:
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
/api/v1/boards/{boardID}/blocks/export:
|
/api/v1/boards/{boardID}/blocks/export:
|
||||||
get:
|
get:
|
||||||
description: Returns all blocks of a board
|
description: Returns all blocks of a board
|
||||||
@ -1276,6 +1318,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/BoardsAndBlocks'
|
$ref: '#/definitions/BoardsAndBlocks'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1327,6 +1371,8 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: success
|
description: success
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1377,6 +1423,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Sharing'
|
$ref: '#/definitions/Sharing'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1633,6 +1681,34 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
summary: Exports an archive of all blocks for all the boards in a team.
|
summary: Exports an archive of all blocks for all the boards in a team.
|
||||||
|
/api/v1/teams/{teamID}/archive/import:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- multipart/form-data
|
||||||
|
operationId: archiveImport
|
||||||
|
parameters:
|
||||||
|
- description: Team ID
|
||||||
|
in: path
|
||||||
|
name: teamID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: archive file to import
|
||||||
|
in: formData
|
||||||
|
name: file
|
||||||
|
required: true
|
||||||
|
type: file
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: success
|
||||||
|
default:
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Import an archive of boards.
|
||||||
/api/v1/teams/{teamID}/boards:
|
/api/v1/teams/{teamID}/boards:
|
||||||
get:
|
get:
|
||||||
description: Returns team boards
|
description: Returns team boards
|
||||||
@ -1686,6 +1762,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/FileUploadResponse'
|
$ref: '#/definitions/FileUploadResponse'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -1697,11 +1775,6 @@ paths:
|
|||||||
description: Returns the boards that match with a search term
|
description: Returns the boards that match with a search term
|
||||||
operationId: searchBoards
|
operationId: searchBoards
|
||||||
parameters:
|
parameters:
|
||||||
- description: Board ID
|
|
||||||
in: path
|
|
||||||
name: boardID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
- description: Team ID
|
- description: Team ID
|
||||||
in: path
|
in: path
|
||||||
name: teamID
|
name: teamID
|
||||||
@ -1919,66 +1992,6 @@ paths:
|
|||||||
$ref: '#/definitions/ErrorResponse'
|
$ref: '#/definitions/ErrorResponse'
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
/api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete:
|
|
||||||
post:
|
|
||||||
description: Undeletes a block
|
|
||||||
operationId: undeleteBlock
|
|
||||||
parameters:
|
|
||||||
- description: Workspace ID
|
|
||||||
in: path
|
|
||||||
name: workspaceID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
- description: ID of block to undelete
|
|
||||||
in: path
|
|
||||||
name: blockID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: success
|
|
||||||
default:
|
|
||||||
description: internal error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
/boards/{boardID}/{rootID}/{fileID}:
|
|
||||||
get:
|
|
||||||
description: Returns the contents of an uploaded file
|
|
||||||
operationId: getFile
|
|
||||||
parameters:
|
|
||||||
- description: Board ID
|
|
||||||
in: path
|
|
||||||
name: boardID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
- description: ID of the root block
|
|
||||||
in: path
|
|
||||||
name: rootID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
- description: ID of the file
|
|
||||||
in: path
|
|
||||||
name: fileID
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
- image/jpg
|
|
||||||
- image/png
|
|
||||||
- image/gif
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: success
|
|
||||||
default:
|
|
||||||
description: internal error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ErrorResponse'
|
|
||||||
security:
|
|
||||||
- BearerAuth: []
|
|
||||||
/boards/{boardID}/join:
|
/boards/{boardID}/join:
|
||||||
post:
|
post:
|
||||||
description: Become a member of a board
|
description: Become a member of a board
|
||||||
@ -1996,10 +2009,35 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/BoardMember'
|
$ref: '#/definitions/BoardMember'
|
||||||
|
"403":
|
||||||
|
description: access denied
|
||||||
"404":
|
"404":
|
||||||
description: board not found
|
description: board not found
|
||||||
"503":
|
default:
|
||||||
|
description: internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ErrorResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
/boards/{boardID}/leave:
|
||||||
|
post:
|
||||||
|
description: Remove your own membership from a board
|
||||||
|
operationId: leaveBoard
|
||||||
|
parameters:
|
||||||
|
- description: Board ID
|
||||||
|
in: path
|
||||||
|
name: boardID
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: success
|
||||||
|
"403":
|
||||||
description: access denied
|
description: access denied
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
@ -2029,6 +2067,8 @@ paths:
|
|||||||
description: success
|
description: success
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/BoardMember'
|
$ref: '#/definitions/BoardMember'
|
||||||
|
"404":
|
||||||
|
description: board not found
|
||||||
default:
|
default:
|
||||||
description: internal error
|
description: internal error
|
||||||
schema:
|
schema:
|
||||||
|
@ -56,10 +56,12 @@ describe('Login actions', () => {
|
|||||||
cy.get('button').contains('Change password').click()
|
cy.get('button').contains('Change password').click()
|
||||||
cy.get('.succeeded').click()
|
cy.get('.succeeded').click()
|
||||||
workspaceIsAvailable()
|
workspaceIsAvailable()
|
||||||
|
logoutUser()
|
||||||
|
|
||||||
// Can log in user with new password
|
// Can log in user with new password
|
||||||
cy.log('**Can log in user with new password**')
|
cy.log('**Can log in user with new password**')
|
||||||
loginUser(newPassword).then(() => resetPassword(newPassword))
|
loginUser(newPassword).then(() => resetPassword(newPassword))
|
||||||
|
logoutUser()
|
||||||
|
|
||||||
// Can't register second user without invite link
|
// Can't register second user without invite link
|
||||||
cy.log('**Can\'t register second user without invite link**')
|
cy.log('**Can\'t register second user without invite link**')
|
||||||
@ -82,10 +84,7 @@ describe('Login actions', () => {
|
|||||||
cy.get('.Button').contains('Copied').should('exist')
|
cy.get('.Button').contains('Copied').should('exist')
|
||||||
|
|
||||||
cy.get('a.shareUrl').invoke('attr', 'href').then((inviteLink) => {
|
cy.get('a.shareUrl').invoke('attr', 'href').then((inviteLink) => {
|
||||||
// Log out existing user
|
logoutUser()
|
||||||
cy.log('**Log out existing user**')
|
|
||||||
cy.get('.Sidebar .SidebarUserMenu').click()
|
|
||||||
cy.get('.menu-name').contains('Log out').click()
|
|
||||||
|
|
||||||
// Register a new user
|
// Register a new user
|
||||||
cy.log('**Register new user**')
|
cy.log('**Register new user**')
|
||||||
@ -112,6 +111,13 @@ describe('Login actions', () => {
|
|||||||
return workspaceIsAvailable()
|
return workspaceIsAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoutUser = () => {
|
||||||
|
cy.log('**Log out existing user**')
|
||||||
|
cy.get('.SidebarUserMenu').click()
|
||||||
|
cy.get('.menu-name').contains('Log out').click()
|
||||||
|
cy.location('pathname').should('eq', '/login')
|
||||||
|
}
|
||||||
|
|
||||||
const resetPassword = (oldPassword: string) => {
|
const resetPassword = (oldPassword: string) => {
|
||||||
cy.apiGetMe().then((userId) => cy.apiChangePassword(userId, oldPassword, password))
|
cy.apiGetMe().then((userId) => cy.apiChangePassword(userId, oldPassword, password))
|
||||||
}
|
}
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Token neu generiert",
|
"ShareBoard.tokenRegenrated": "Token neu generiert",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Mitglied entfernen",
|
"ShareBoard.userPermissionsRemoveMemberText": "Mitglied entfernen",
|
||||||
"ShareBoard.userPermissionsYouText": "(Du)",
|
"ShareBoard.userPermissionsYouText": "(Du)",
|
||||||
|
"ShareTemplate.Title": "Vorlage teilen",
|
||||||
"Sidebar.about": "Über Focalboard",
|
"Sidebar.about": "Über Focalboard",
|
||||||
"Sidebar.add-board": "+ Board hinzufügen",
|
"Sidebar.add-board": "+ Board hinzufügen",
|
||||||
"Sidebar.changePassword": "Passwort ändern",
|
"Sidebar.changePassword": "Passwort ändern",
|
||||||
"Sidebar.delete-board": "Board löschen",
|
"Sidebar.delete-board": "Board löschen",
|
||||||
|
"Sidebar.duplicate-board": "Board kopieren",
|
||||||
"Sidebar.export-archive": "Archiv exportieren",
|
"Sidebar.export-archive": "Archiv exportieren",
|
||||||
"Sidebar.import": "Importieren",
|
"Sidebar.import": "Importieren",
|
||||||
"Sidebar.import-archive": "Archiv importieren",
|
"Sidebar.import-archive": "Archiv importieren",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Sprache übernehmen",
|
"Sidebar.set-language": "Sprache übernehmen",
|
||||||
"Sidebar.set-theme": "Theme übernehmen",
|
"Sidebar.set-theme": "Theme übernehmen",
|
||||||
"Sidebar.settings": "Einstellungen",
|
"Sidebar.settings": "Einstellungen",
|
||||||
|
"Sidebar.template-from-board": "Neue Vorlage aus Board",
|
||||||
"Sidebar.untitled-board": "(Unbenanntes Board)",
|
"Sidebar.untitled-board": "(Unbenanntes Board)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Bewege nach...",
|
"SidebarCategories.BlocksMenu.Move": "Bewege nach...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Erstelle neue Kategorie",
|
"SidebarCategories.CategoryMenu.CreateNew": "Erstelle neue Kategorie",
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Token regenerated",
|
"ShareBoard.tokenRegenrated": "Token regenerated",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Remove member",
|
"ShareBoard.userPermissionsRemoveMemberText": "Remove member",
|
||||||
"ShareBoard.userPermissionsYouText": "(You)",
|
"ShareBoard.userPermissionsYouText": "(You)",
|
||||||
|
"ShareTemplate.Title": "Share Template",
|
||||||
"Sidebar.about": "About Focalboard",
|
"Sidebar.about": "About Focalboard",
|
||||||
"Sidebar.add-board": "+ Add board",
|
"Sidebar.add-board": "+ Add board",
|
||||||
"Sidebar.changePassword": "Change password",
|
"Sidebar.changePassword": "Change password",
|
||||||
"Sidebar.delete-board": "Delete board",
|
"Sidebar.delete-board": "Delete board",
|
||||||
|
"Sidebar.duplicate-board": "Duplicate board",
|
||||||
"Sidebar.export-archive": "Export archive",
|
"Sidebar.export-archive": "Export archive",
|
||||||
"Sidebar.import": "Import",
|
"Sidebar.import": "Import",
|
||||||
"Sidebar.import-archive": "Import archive",
|
"Sidebar.import-archive": "Import archive",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Set language",
|
"Sidebar.set-language": "Set language",
|
||||||
"Sidebar.set-theme": "Set theme",
|
"Sidebar.set-theme": "Set theme",
|
||||||
"Sidebar.settings": "Settings",
|
"Sidebar.settings": "Settings",
|
||||||
|
"Sidebar.template-from-board": "New template from board",
|
||||||
"Sidebar.untitled-board": "(Untitled Board)",
|
"Sidebar.untitled-board": "(Untitled Board)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Move To...",
|
"SidebarCategories.BlocksMenu.Move": "Move To...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
|
"SidebarCategories.CategoryMenu.CreateNew": "Create New Category",
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
"BoardComponent.delete": "Supprimer",
|
"BoardComponent.delete": "Supprimer",
|
||||||
"BoardComponent.hidden-columns": "Colonnes cachées",
|
"BoardComponent.hidden-columns": "Colonnes cachées",
|
||||||
"BoardComponent.hide": "Cacher",
|
"BoardComponent.hide": "Cacher",
|
||||||
"BoardComponent.new": "Nouveau",
|
"BoardComponent.new": "+ Nouveau",
|
||||||
"BoardComponent.no-property": "Pas de {property}",
|
"BoardComponent.no-property": "Pas de {property}",
|
||||||
"BoardComponent.no-property-title": "Les éléments sans propriété {property} seront placés ici. Cette colonne ne peut pas être supprimée.",
|
"BoardComponent.no-property-title": "Les éléments sans propriété {property} seront placés ici. Cette colonne ne peut pas être supprimée.",
|
||||||
"BoardComponent.show": "Montrer",
|
"BoardComponent.show": "Montrer",
|
||||||
|
"BoardMember.schemeAdmin": "Admin",
|
||||||
|
"BoardMember.schemeEditor": "Éditeur",
|
||||||
|
"BoardMember.schemeNone": "Aucun",
|
||||||
"BoardPage.newVersion": "Une nouvelle version de Boards est disponible, cliquez ici pour recharger.",
|
"BoardPage.newVersion": "Une nouvelle version de Boards est disponible, cliquez ici pour recharger.",
|
||||||
"BoardPage.syncFailed": "Le tableau a peut-être été supprimé ou vos droits d'accès révoqués.",
|
"BoardPage.syncFailed": "Le tableau a peut-être été supprimé ou vos droits d'accès révoqués.",
|
||||||
"BoardTemplateSelector.add-template": "Nouveau modèle",
|
"BoardTemplateSelector.add-template": "Nouveau modèle",
|
||||||
@ -14,12 +17,45 @@
|
|||||||
"BoardTemplateSelector.delete-template": "Supprimer",
|
"BoardTemplateSelector.delete-template": "Supprimer",
|
||||||
"BoardTemplateSelector.description": "Choisissez un modèle pour vous aider à démarrer. Personnalisez facilement le modèle en fonction de vos besoins ou créez un tableau vide pour repartir de zéro.",
|
"BoardTemplateSelector.description": "Choisissez un modèle pour vous aider à démarrer. Personnalisez facilement le modèle en fonction de vos besoins ou créez un tableau vide pour repartir de zéro.",
|
||||||
"BoardTemplateSelector.edit-template": "Éditer",
|
"BoardTemplateSelector.edit-template": "Éditer",
|
||||||
"BoardTemplateSelector.plugin.no-content-description": "Ajouter un tableau à la barre latérale en utilisant l'un des modèles définis ci-dessous ou recommencer à zéro. {lineBreak} Les membres de \"{workspaceName}\" auront accès aux tableaux créés ici.",
|
"BoardTemplateSelector.plugin.no-content-description": "Ajouter un tableau à la barre latérale en utilisant l'un des modèles ci-dessous ou commencer à partir de zéro. {lineBreak} Les membres de \"{teamName}\" auront accès aux tableaux créés ici.",
|
||||||
"BoardTemplateSelector.plugin.no-content-title": "Créer un tableau dans {workspaceName}",
|
"BoardTemplateSelector.plugin.no-content-title": "Créer un tableau dans {teamName}",
|
||||||
"BoardTemplateSelector.title": "Créer un tableau",
|
"BoardTemplateSelector.title": "Créer un tableau",
|
||||||
"BoardTemplateSelector.use-this-template": "Utiliser ce modèle",
|
"BoardTemplateSelector.use-this-template": "Utiliser ce modèle",
|
||||||
|
"BoardsSwitcher.Title": "Rechercher des tableaux",
|
||||||
|
"BoardsUnfurl.Remainder": "+{remainder} plus",
|
||||||
"BoardsUnfurl.Updated": "Mis à jour {time}",
|
"BoardsUnfurl.Updated": "Mis à jour {time}",
|
||||||
|
"Calculations.Options.average.displayName": "Moyenne",
|
||||||
|
"Calculations.Options.average.label": "Moyenne",
|
||||||
|
"Calculations.Options.count.displayName": "Compter",
|
||||||
|
"Calculations.Options.count.label": "Compter",
|
||||||
|
"Calculations.Options.countChecked.displayName": "Coché",
|
||||||
|
"Calculations.Options.countChecked.label": "Total coché",
|
||||||
|
"Calculations.Options.countUnchecked.displayName": "Décoché",
|
||||||
|
"Calculations.Options.countUnchecked.label": "Total décoché",
|
||||||
|
"Calculations.Options.countUniqueValue.displayName": "Unique",
|
||||||
|
"Calculations.Options.countUniqueValue.label": "Compter les valeurs uniques",
|
||||||
|
"Calculations.Options.countValue.displayName": "Valeurs",
|
||||||
|
"Calculations.Options.countValue.label": "Valeur",
|
||||||
|
"Calculations.Options.dateRange.displayName": "Il y a",
|
||||||
|
"Calculations.Options.dateRange.label": "Intervalle",
|
||||||
|
"Calculations.Options.earliest.displayName": "Plus ancien",
|
||||||
|
"Calculations.Options.earliest.label": "Plus ancien",
|
||||||
|
"Calculations.Options.latest.displayName": "Plus récent",
|
||||||
|
"Calculations.Options.latest.label": "Plus récent",
|
||||||
|
"Calculations.Options.max.displayName": "Maximum",
|
||||||
|
"Calculations.Options.max.label": "Maximum",
|
||||||
|
"Calculations.Options.median.displayName": "Médiane",
|
||||||
|
"Calculations.Options.median.label": "Médiane",
|
||||||
|
"Calculations.Options.min.displayName": "Minimum",
|
||||||
|
"Calculations.Options.min.label": "Minimum",
|
||||||
|
"Calculations.Options.none.displayName": "Calculer",
|
||||||
"Calculations.Options.none.label": "Aucun",
|
"Calculations.Options.none.label": "Aucun",
|
||||||
|
"Calculations.Options.percentChecked.displayName": "Coché",
|
||||||
|
"Calculations.Options.percentChecked.label": "Coché (%)",
|
||||||
|
"Calculations.Options.percentUnchecked.displayName": "Décoché",
|
||||||
|
"Calculations.Options.percentUnchecked.label": "Décoché (%)",
|
||||||
|
"Calculations.Options.range.displayName": "Curseur",
|
||||||
|
"Calculations.Options.range.label": "Curseur",
|
||||||
"Calculations.Options.sum.displayName": "Somme",
|
"Calculations.Options.sum.displayName": "Somme",
|
||||||
"Calculations.Options.sum.label": "Somme",
|
"Calculations.Options.sum.label": "Somme",
|
||||||
"CardBadges.title-checkboxes": "Cases à cocher",
|
"CardBadges.title-checkboxes": "Cases à cocher",
|
||||||
@ -35,17 +71,24 @@
|
|||||||
"CardDetail.new-comment-placeholder": "Ajouter un commentaire...",
|
"CardDetail.new-comment-placeholder": "Ajouter un commentaire...",
|
||||||
"CardDetailProperty.confirm-delete-heading": "Confirmer la suppression de la propriété",
|
"CardDetailProperty.confirm-delete-heading": "Confirmer la suppression de la propriété",
|
||||||
"CardDetailProperty.confirm-delete-subtext": "Êtes-vous sûr de vouloir supprimer la propriété « {propertyName} » ? La suppression retirera la propriété de toutes les cartes dans ce tableau.",
|
"CardDetailProperty.confirm-delete-subtext": "Êtes-vous sûr de vouloir supprimer la propriété « {propertyName} » ? La suppression retirera la propriété de toutes les cartes dans ce tableau.",
|
||||||
|
"CardDetailProperty.confirm-property-name-change-subtext": "Voulez-vous vraiment modifier le type de la propriété \"{propertyName}\" {customText} ? Cela affectera la ou les valeur(s) sur {numOfCards} carte(s) dans ce tableau et peut entraîner une perte de données.",
|
||||||
"CardDetailProperty.confirm-property-type-change": "Confirmer le changement de type de propriété !",
|
"CardDetailProperty.confirm-property-type-change": "Confirmer le changement de type de propriété !",
|
||||||
"CardDetailProperty.delete-action-button": "Supprimer",
|
"CardDetailProperty.delete-action-button": "Supprimer",
|
||||||
"CardDetailProperty.property-change-action-button": "Modifier la propriété",
|
"CardDetailProperty.property-change-action-button": "Modifier la propriété",
|
||||||
"CardDetailProperty.property-changed": "Propriété modifiée avec succès !",
|
"CardDetailProperty.property-changed": "Propriété modifiée avec succès !",
|
||||||
"CardDetailProperty.property-deleted": "{propertyName} supprimé avec succès !",
|
"CardDetailProperty.property-deleted": "{propertyName} supprimé avec succès !",
|
||||||
|
"CardDetailProperty.property-name-change-subtext": "de \"{oldPropType}\" à \"{newPropType}\"",
|
||||||
|
"CardDetailProperty.property-type-change-subtext": "nom de \"{newPropName}\"",
|
||||||
"CardDialog.copiedLink": "Copié !",
|
"CardDialog.copiedLink": "Copié !",
|
||||||
"CardDialog.copyLink": "Copier le lien",
|
"CardDialog.copyLink": "Copier le lien",
|
||||||
"CardDialog.delete-confirmation-dialog-button-text": "Supprimer",
|
"CardDialog.delete-confirmation-dialog-button-text": "Supprimer",
|
||||||
"CardDialog.delete-confirmation-dialog-heading": "Confirmer la suppression de la carte !",
|
"CardDialog.delete-confirmation-dialog-heading": "Confirmer la suppression de la carte !",
|
||||||
"CardDialog.editing-template": "Vous éditez un modèle.",
|
"CardDialog.editing-template": "Vous éditez un modèle.",
|
||||||
"CardDialog.nocard": "Cette carte n'existe pas ou n'est pas accessible.",
|
"CardDialog.nocard": "Cette carte n'existe pas ou n'est pas accessible.",
|
||||||
|
"Categories.CreateCategoryDialog.CancelText": "Annuler",
|
||||||
|
"Categories.CreateCategoryDialog.CreateText": "Créer",
|
||||||
|
"Categories.CreateCategoryDialog.Placeholder": "Nommez votre catégorie",
|
||||||
|
"Categories.CreateCategoryDialog.UpdateText": "Mettre à jour",
|
||||||
"CenterPanel.Share": "Partager",
|
"CenterPanel.Share": "Partager",
|
||||||
"ColorOption.selectColor": "Choisir la couleur {color}",
|
"ColorOption.selectColor": "Choisir la couleur {color}",
|
||||||
"Comment.delete": "Supprimer",
|
"Comment.delete": "Supprimer",
|
||||||
@ -81,11 +124,18 @@
|
|||||||
"Filter.not-includes": "n'inclut pas",
|
"Filter.not-includes": "n'inclut pas",
|
||||||
"FilterComponent.add-filter": "+ Ajouter un filtre",
|
"FilterComponent.add-filter": "+ Ajouter un filtre",
|
||||||
"FilterComponent.delete": "Supprimer",
|
"FilterComponent.delete": "Supprimer",
|
||||||
|
"FindBoFindBoardsDialog.IntroText": "Rechercher des tableaux",
|
||||||
|
"FindBoardsDialog.NoResultsFor": "Pas de résultats pour \"{searchQuery}\"",
|
||||||
|
"FindBoardsDialog.NoResultsSubtext": "Vérifiez l'orthographe ou essayez une autre recherche.",
|
||||||
|
"FindBoardsDialog.SubTitle": "Recherchez ci-dessous pour trouver un tableau. Utilisez <b>HAUT/BAS</b> pour naviguer. <b>ENTRER</b> pour sélectionner, <b>ECHAP</b> pour annuler",
|
||||||
|
"FindBoardsDialog.Title": "Rechercher des tableaux",
|
||||||
"GalleryCard.copiedLink": "Copié !",
|
"GalleryCard.copiedLink": "Copié !",
|
||||||
"GalleryCard.copyLink": "Copier le lien",
|
"GalleryCard.copyLink": "Copier le lien",
|
||||||
"GalleryCard.delete": "Supprimer",
|
"GalleryCard.delete": "Supprimer",
|
||||||
"GalleryCard.duplicate": "Dupliquer",
|
"GalleryCard.duplicate": "Dupliquer",
|
||||||
"General.BoardCount": "{count, plural, one {# Tableau} other {# Tableaux}}",
|
"General.BoardCount": "{count, plural, one {# Tableau} other {# Tableaux}}",
|
||||||
|
"GroupBy.hideEmptyGroups": "Masquer {count} groupes vides",
|
||||||
|
"GroupBy.showHiddenGroups": "Afficher les {count} groupes masqués",
|
||||||
"GroupBy.ungroup": "Dégrouper",
|
"GroupBy.ungroup": "Dégrouper",
|
||||||
"KanbanCard.copiedLink": "Copié !",
|
"KanbanCard.copiedLink": "Copié !",
|
||||||
"KanbanCard.copyLink": "Copier le lien",
|
"KanbanCard.copyLink": "Copier le lien",
|
||||||
@ -141,21 +191,34 @@
|
|||||||
"ShareBoard.copiedLink": "Copié !",
|
"ShareBoard.copiedLink": "Copié !",
|
||||||
"ShareBoard.copyLink": "Copier le lien",
|
"ShareBoard.copyLink": "Copier le lien",
|
||||||
"ShareBoard.regenerate": "Régénérer le jeton",
|
"ShareBoard.regenerate": "Régénérer le jeton",
|
||||||
|
"ShareBoard.teamPermissionsText": "Tout le monde à l'équipe {teamName}",
|
||||||
"ShareBoard.tokenRegenrated": "Le jeton a été recréé",
|
"ShareBoard.tokenRegenrated": "Le jeton a été recréé",
|
||||||
|
"ShareBoard.userPermissionsRemoveMemberText": "Supprimer un membre",
|
||||||
|
"ShareBoard.userPermissionsYouText": "(Vous)",
|
||||||
|
"ShareTemplate.Title": "Partager un modèle",
|
||||||
"Sidebar.about": "À propos de Focalboard",
|
"Sidebar.about": "À propos de Focalboard",
|
||||||
"Sidebar.add-board": "+ Ajouter un tableau",
|
"Sidebar.add-board": "+ Ajouter un tableau",
|
||||||
"Sidebar.changePassword": "Modifier le mot de passe",
|
"Sidebar.changePassword": "Modifier le mot de passe",
|
||||||
"Sidebar.delete-board": "Supprimer le tableau",
|
"Sidebar.delete-board": "Supprimer le tableau",
|
||||||
|
"Sidebar.duplicate-board": "Dupliquer une carte",
|
||||||
"Sidebar.export-archive": "Exporter une archive",
|
"Sidebar.export-archive": "Exporter une archive",
|
||||||
"Sidebar.import": "Importer",
|
"Sidebar.import": "Importer",
|
||||||
"Sidebar.import-archive": "Importer une archive",
|
"Sidebar.import-archive": "Importer une archive",
|
||||||
"Sidebar.invite-users": "Inviter des utilisateurs",
|
"Sidebar.invite-users": "Inviter des utilisateurs",
|
||||||
"Sidebar.logout": "Se déconnecter",
|
"Sidebar.logout": "Se déconnecter",
|
||||||
|
"Sidebar.no-boards-in-category": "Aucun tableaux",
|
||||||
"Sidebar.random-icons": "Icônes aléatoires",
|
"Sidebar.random-icons": "Icônes aléatoires",
|
||||||
"Sidebar.set-language": "Choisir la langue",
|
"Sidebar.set-language": "Choisir la langue",
|
||||||
"Sidebar.set-theme": "Choisir le thème",
|
"Sidebar.set-theme": "Choisir le thème",
|
||||||
"Sidebar.settings": "Réglages",
|
"Sidebar.settings": "Réglages",
|
||||||
|
"Sidebar.template-from-board": "Nouveau modèle de tableau",
|
||||||
"Sidebar.untitled-board": "(Tableau sans titre)",
|
"Sidebar.untitled-board": "(Tableau sans titre)",
|
||||||
|
"SidebarCategories.BlocksMenu.Move": "Déplacer vers ...",
|
||||||
|
"SidebarCategories.CategoryMenu.CreateNew": "Créer une nouvelle catégorie",
|
||||||
|
"SidebarCategories.CategoryMenu.Delete": "Supprimer la catégorie",
|
||||||
|
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Les tableaux de <b>{categoryName}</b> reviendront à la catégorie par défaut. Aucun des tableaux ne seront supprimés.",
|
||||||
|
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Supprimer cette catégorie ?",
|
||||||
|
"SidebarCategories.CategoryMenu.Update": "Renommer la catégorie",
|
||||||
"TableComponent.add-icon": "Ajouter une icône",
|
"TableComponent.add-icon": "Ajouter une icône",
|
||||||
"TableComponent.name": "Nom",
|
"TableComponent.name": "Nom",
|
||||||
"TableComponent.plus-new": "+ Nouveau",
|
"TableComponent.plus-new": "+ Nouveau",
|
||||||
@ -201,6 +264,7 @@
|
|||||||
"ViewHeader.set-default-template": "Définir par défaut",
|
"ViewHeader.set-default-template": "Définir par défaut",
|
||||||
"ViewHeader.sort": "Trier",
|
"ViewHeader.sort": "Trier",
|
||||||
"ViewHeader.untitled": "Sans titre",
|
"ViewHeader.untitled": "Sans titre",
|
||||||
|
"ViewHeader.view-header-menu": "Afficher le menu d'en-tête",
|
||||||
"ViewHeader.view-menu": "Afficher le menu",
|
"ViewHeader.view-menu": "Afficher le menu",
|
||||||
"ViewTitle.hide-description": "cacher la description",
|
"ViewTitle.hide-description": "cacher la description",
|
||||||
"ViewTitle.pick-icon": "Choisir une icône",
|
"ViewTitle.pick-icon": "Choisir une icône",
|
||||||
@ -208,8 +272,8 @@
|
|||||||
"ViewTitle.remove-icon": "Supprimer l'icône",
|
"ViewTitle.remove-icon": "Supprimer l'icône",
|
||||||
"ViewTitle.show-description": "montrer la description",
|
"ViewTitle.show-description": "montrer la description",
|
||||||
"ViewTitle.untitled-board": "Tableau sans titre",
|
"ViewTitle.untitled-board": "Tableau sans titre",
|
||||||
"WelcomePage.Description": "Boards est un outil de gestion de projet qui permet de d'organiser, de suivre et de gérer le travail entre équipes, utilisant des tableaux Kanban",
|
"WelcomePage.Description": "Boards est un outil de gestion de projet qui permet d'organiser, de suivre et de gérer le travail entre équipes, utilisant des tableaux Kanban",
|
||||||
"WelcomePage.Explore.Button": "Explorer",
|
"WelcomePage.Explore.Button": "Tutoriel",
|
||||||
"WelcomePage.Heading": "Bienvenue sur Boards",
|
"WelcomePage.Heading": "Bienvenue sur Boards",
|
||||||
"WelcomePage.NoThanks.Text": "Non merci, je vais me renseigner moi-même",
|
"WelcomePage.NoThanks.Text": "Non merci, je vais me renseigner moi-même",
|
||||||
"Workspace.editing-board-template": "Vous éditez un modèle de tableau.",
|
"Workspace.editing-board-template": "Vous éditez un modèle de tableau.",
|
||||||
@ -232,6 +296,7 @@
|
|||||||
"login.register-button": "ou créez un compte si vous n'en avez pas",
|
"login.register-button": "ou créez un compte si vous n'en avez pas",
|
||||||
"register.login-button": "ou connectez-vous si vous avez déjà un compte",
|
"register.login-button": "ou connectez-vous si vous avez déjà un compte",
|
||||||
"register.signup-title": "Inscrivez-vous pour créer un compte",
|
"register.signup-title": "Inscrivez-vous pour créer un compte",
|
||||||
|
"shareBoard.lastAdmin": "Les conseils doivent avoir au moins un administrateur",
|
||||||
"tutorial_tip.finish_tour": "Terminé",
|
"tutorial_tip.finish_tour": "Terminé",
|
||||||
"tutorial_tip.got_it": "J'ai compris",
|
"tutorial_tip.got_it": "J'ai compris",
|
||||||
"tutorial_tip.ok": "Suivant",
|
"tutorial_tip.ok": "Suivant",
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Token je ponovo generiran",
|
"ShareBoard.tokenRegenrated": "Token je ponovo generiran",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Ukloni člana",
|
"ShareBoard.userPermissionsRemoveMemberText": "Ukloni člana",
|
||||||
"ShareBoard.userPermissionsYouText": "(Ti)",
|
"ShareBoard.userPermissionsYouText": "(Ti)",
|
||||||
|
"ShareTemplate.Title": "Dijeli predložak",
|
||||||
"Sidebar.about": "O programu Focalboard",
|
"Sidebar.about": "O programu Focalboard",
|
||||||
"Sidebar.add-board": "+ Dodaj ploču",
|
"Sidebar.add-board": "+ Dodaj ploču",
|
||||||
"Sidebar.changePassword": "Promijeni lozinku",
|
"Sidebar.changePassword": "Promijeni lozinku",
|
||||||
"Sidebar.delete-board": "Izbriši ploču",
|
"Sidebar.delete-board": "Izbriši ploču",
|
||||||
|
"Sidebar.duplicate-board": "Dupliciraj ploču",
|
||||||
"Sidebar.export-archive": "Izvezi arhivu",
|
"Sidebar.export-archive": "Izvezi arhivu",
|
||||||
"Sidebar.import": "Uvezi",
|
"Sidebar.import": "Uvezi",
|
||||||
"Sidebar.import-archive": "Uvezi arhivu",
|
"Sidebar.import-archive": "Uvezi arhivu",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Postavi jezik",
|
"Sidebar.set-language": "Postavi jezik",
|
||||||
"Sidebar.set-theme": "Postavi temu",
|
"Sidebar.set-theme": "Postavi temu",
|
||||||
"Sidebar.settings": "Postavke",
|
"Sidebar.settings": "Postavke",
|
||||||
|
"Sidebar.template-from-board": "Novi predložak iz ploče",
|
||||||
"Sidebar.untitled-board": "(Ploča bez naslova)",
|
"Sidebar.untitled-board": "(Ploča bez naslova)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Premjesti u …",
|
"SidebarCategories.BlocksMenu.Move": "Premjesti u …",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Stvori novu kategoriju",
|
"SidebarCategories.CategoryMenu.CreateNew": "Stvori novu kategoriju",
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
"BoardComponent.no-property": "No {property}",
|
"BoardComponent.no-property": "No {property}",
|
||||||
"BoardComponent.no-property-title": "Gli oggetti senza alcuna proprietà {property} andranno qui. Questo campo non può essere rimosso.",
|
"BoardComponent.no-property-title": "Gli oggetti senza alcuna proprietà {property} andranno qui. Questo campo non può essere rimosso.",
|
||||||
"BoardComponent.show": "Mostra",
|
"BoardComponent.show": "Mostra",
|
||||||
|
"BoardMember.schemeAdmin": "Amministratore",
|
||||||
|
"BoardMember.schemeEditor": "Editor",
|
||||||
|
"BoardMember.schemeNone": "Niente",
|
||||||
"BoardPage.newVersion": "Una nuova versione di Board è disponibile, clicca qui per ricaricare.",
|
"BoardPage.newVersion": "Una nuova versione di Board è disponibile, clicca qui per ricaricare.",
|
||||||
"BoardPage.syncFailed": "La board potrebbe essere cancellata o l'accesso revocato.",
|
"BoardPage.syncFailed": "La board potrebbe essere cancellata o l'accesso revocato.",
|
||||||
"BoardTemplateSelector.add-template": "Nuovo modello",
|
"BoardTemplateSelector.add-template": "Nuovo modello",
|
||||||
@ -14,10 +17,11 @@
|
|||||||
"BoardTemplateSelector.delete-template": "Elimina",
|
"BoardTemplateSelector.delete-template": "Elimina",
|
||||||
"BoardTemplateSelector.description": "Scegli un modello per iniziare. Personalizza facilmente il modello in base alle tue esigenze o crea una board vuota per iniziare da zero.",
|
"BoardTemplateSelector.description": "Scegli un modello per iniziare. Personalizza facilmente il modello in base alle tue esigenze o crea una board vuota per iniziare da zero.",
|
||||||
"BoardTemplateSelector.edit-template": "Modifica",
|
"BoardTemplateSelector.edit-template": "Modifica",
|
||||||
"BoardTemplateSelector.plugin.no-content-description": "Aggiungi una bacheca alla barra laterale utilizzando uno dei modelli definiti di seguito o inizia da zero.{lineBreak} I membri di \"{workspaceName}\" avranno accesso alle bacheche create qui.",
|
"BoardTemplateSelector.plugin.no-content-description": "Aggiungi una bacheca alla barra laterale utilizzando uno dei modelli definiti di seguito o inizia da zero.{lineBreak} I membri di \"{teamName}\" avranno accesso alle bacheche create qui.",
|
||||||
"BoardTemplateSelector.plugin.no-content-title": "Crea una bacheca in {workspaceName}",
|
"BoardTemplateSelector.plugin.no-content-title": "Crea una bacheca in {teamName}",
|
||||||
"BoardTemplateSelector.title": "Crea una bacheca",
|
"BoardTemplateSelector.title": "Crea una bacheca",
|
||||||
"BoardTemplateSelector.use-this-template": "Usa questo modello",
|
"BoardTemplateSelector.use-this-template": "Usa questo modello",
|
||||||
|
"BoardsSwitcher.Title": "Trova bacheche",
|
||||||
"BoardsUnfurl.Remainder": "+{remainder} di più",
|
"BoardsUnfurl.Remainder": "+{remainder} di più",
|
||||||
"BoardsUnfurl.Updated": "Aggiornato {time}",
|
"BoardsUnfurl.Updated": "Aggiornato {time}",
|
||||||
"Calculations.Options.average.displayName": "Media",
|
"Calculations.Options.average.displayName": "Media",
|
||||||
@ -81,6 +85,10 @@
|
|||||||
"CardDialog.delete-confirmation-dialog-heading": "Conferma l'eliminazione della scheda!",
|
"CardDialog.delete-confirmation-dialog-heading": "Conferma l'eliminazione della scheda!",
|
||||||
"CardDialog.editing-template": "Stai modificando un template.",
|
"CardDialog.editing-template": "Stai modificando un template.",
|
||||||
"CardDialog.nocard": "Questa scheda non esiste o è inaccessibile.",
|
"CardDialog.nocard": "Questa scheda non esiste o è inaccessibile.",
|
||||||
|
"Categories.CreateCategoryDialog.CancelText": "Cancella",
|
||||||
|
"Categories.CreateCategoryDialog.CreateText": "Crea",
|
||||||
|
"Categories.CreateCategoryDialog.Placeholder": "Nomina la tua categoria",
|
||||||
|
"Categories.CreateCategoryDialog.UpdateText": "Aggiorna",
|
||||||
"CenterPanel.Share": "Condividi",
|
"CenterPanel.Share": "Condividi",
|
||||||
"ColorOption.selectColor": "Seleziona{color} Colore",
|
"ColorOption.selectColor": "Seleziona{color} Colore",
|
||||||
"Comment.delete": "Elimina",
|
"Comment.delete": "Elimina",
|
||||||
@ -116,11 +124,18 @@
|
|||||||
"Filter.not-includes": "non include",
|
"Filter.not-includes": "non include",
|
||||||
"FilterComponent.add-filter": "+ Aggiungi un filtro",
|
"FilterComponent.add-filter": "+ Aggiungi un filtro",
|
||||||
"FilterComponent.delete": "Elimina",
|
"FilterComponent.delete": "Elimina",
|
||||||
|
"FindBoFindBoardsDialog.IntroText": "Cerca bacheche",
|
||||||
|
"FindBoardsDialog.NoResultsFor": "Nessun risultato per \"{searchQuery}\"",
|
||||||
|
"FindBoardsDialog.NoResultsSubtext": "Controlla l'ortografia o prova con un'altra ricerca.",
|
||||||
|
"FindBoardsDialog.SubTitle": "Scrivi per trovare una bacheca. Utilizza i tasti <b>SU/GIÙ</b> per navigare. <b>INVIO</b> per selezionare, <b>ESC</b> per annullare",
|
||||||
|
"FindBoardsDialog.Title": "Trova bacheche",
|
||||||
"GalleryCard.copiedLink": "Copiato!",
|
"GalleryCard.copiedLink": "Copiato!",
|
||||||
"GalleryCard.copyLink": "Copia link",
|
"GalleryCard.copyLink": "Copia link",
|
||||||
"GalleryCard.delete": "Elimina",
|
"GalleryCard.delete": "Elimina",
|
||||||
"GalleryCard.duplicate": "Duplica",
|
"GalleryCard.duplicate": "Duplica",
|
||||||
"General.BoardCount": "{count, plural, one {# Board} other {# Boards}}",
|
"General.BoardCount": "{count, plural, one {# Board} other {# Boards}}",
|
||||||
|
"GroupBy.hideEmptyGroups": "Nascondi {count} gruppi vuoti",
|
||||||
|
"GroupBy.showHiddenGroups": "Mostra {count} gruppi nascosti",
|
||||||
"GroupBy.ungroup": "Dividi",
|
"GroupBy.ungroup": "Dividi",
|
||||||
"KanbanCard.copiedLink": "Copiato!",
|
"KanbanCard.copiedLink": "Copiato!",
|
||||||
"KanbanCard.copyLink": "Copia link",
|
"KanbanCard.copyLink": "Copia link",
|
||||||
@ -129,6 +144,20 @@
|
|||||||
"KanbanCard.untitled": "Senza titolo",
|
"KanbanCard.untitled": "Senza titolo",
|
||||||
"Mutator.new-card-from-template": "nuova scheda da modello",
|
"Mutator.new-card-from-template": "nuova scheda da modello",
|
||||||
"Mutator.new-template-from-card": "nuovo modello da scheda",
|
"Mutator.new-template-from-card": "nuovo modello da scheda",
|
||||||
|
"OnboardingTour.AddComments.Body": "Puoi commentare sui issues e anche @menzionare il tuo compagno su Mattermost per attirare la loro attenzione.",
|
||||||
|
"OnboardingTour.AddComments.Title": "Aggiungi commenti",
|
||||||
|
"OnboardingTour.AddDescription.Body": "Aggiungi una descrizione alla tua scheda in modo da far sapere che cosa riguarda.",
|
||||||
|
"OnboardingTour.AddDescription.Title": "Aggiungi una descrizione",
|
||||||
|
"OnboardingTour.AddProperties.Body": "Aggiungi varie proprietà alle schede per renderle ancora più potenti!",
|
||||||
|
"OnboardingTour.AddProperties.Title": "Aggiungi proprietà",
|
||||||
|
"OnboardingTour.AddView.Body": "Vai qui in modo da creare una nuova vista per organizzare le tue bacheche utilizzando differenti layout.",
|
||||||
|
"OnboardingTour.AddView.Title": "Aggiungi una nuova vista",
|
||||||
|
"OnboardingTour.CopyLink.Body": "Puoi condividere le schede con i tuoi colleghi copiando il link e incollandolo in un canale, messaggio privato o messaggio di gruppo.",
|
||||||
|
"OnboardingTour.CopyLink.Title": "Copia link",
|
||||||
|
"OnboardingTour.OpenACard.Body": "Apri una scheda per esplorare i potenti modi in cui una Bacheca può aiutarti nell'organizzare il lavoro.",
|
||||||
|
"OnboardingTour.OpenACard.Title": "Apri una scheda",
|
||||||
|
"OnboardingTour.ShareBoard.Body": "Puoi condividere la bacheca internamente, solo con il tuo team, oppure pubblicarlo per tutti gli utenti fuori dalla tua organizzazione.",
|
||||||
|
"OnboardingTour.ShareBoard.Title": "Condividi bacheca",
|
||||||
"PropertyMenu.Delete": "Elimina",
|
"PropertyMenu.Delete": "Elimina",
|
||||||
"PropertyMenu.changeType": "Cambia il tipo di proprietà",
|
"PropertyMenu.changeType": "Cambia il tipo di proprietà",
|
||||||
"PropertyMenu.selectType": "Seleziona il tipo di proprietà",
|
"PropertyMenu.selectType": "Seleziona il tipo di proprietà",
|
||||||
@ -162,20 +191,34 @@
|
|||||||
"ShareBoard.copiedLink": "Copiato!",
|
"ShareBoard.copiedLink": "Copiato!",
|
||||||
"ShareBoard.copyLink": "Copia link",
|
"ShareBoard.copyLink": "Copia link",
|
||||||
"ShareBoard.regenerate": "Rigenera token",
|
"ShareBoard.regenerate": "Rigenera token",
|
||||||
|
"ShareBoard.teamPermissionsText": "Tutti nel team {teamName}",
|
||||||
"ShareBoard.tokenRegenrated": "Token rigenerato",
|
"ShareBoard.tokenRegenrated": "Token rigenerato",
|
||||||
|
"ShareBoard.userPermissionsRemoveMemberText": "Rimuovi utente",
|
||||||
|
"ShareBoard.userPermissionsYouText": "(Tu)",
|
||||||
|
"ShareTemplate.Title": "Condividi template",
|
||||||
"Sidebar.about": "Informazioni su Focalboard",
|
"Sidebar.about": "Informazioni su Focalboard",
|
||||||
"Sidebar.add-board": "+ Aggiungi Contenitore",
|
"Sidebar.add-board": "+ Aggiungi Contenitore",
|
||||||
"Sidebar.changePassword": "Cambia password",
|
"Sidebar.changePassword": "Cambia password",
|
||||||
"Sidebar.delete-board": "Elimina contenitore",
|
"Sidebar.delete-board": "Elimina contenitore",
|
||||||
|
"Sidebar.duplicate-board": "Duplica bacheca",
|
||||||
"Sidebar.export-archive": "Esporta archivio",
|
"Sidebar.export-archive": "Esporta archivio",
|
||||||
|
"Sidebar.import": "Importa",
|
||||||
"Sidebar.import-archive": "Importa archivio",
|
"Sidebar.import-archive": "Importa archivio",
|
||||||
"Sidebar.invite-users": "Invita utenti",
|
"Sidebar.invite-users": "Invita utenti",
|
||||||
"Sidebar.logout": "Logout",
|
"Sidebar.logout": "Logout",
|
||||||
|
"Sidebar.no-boards-in-category": "Nessuna bacheca all'interno",
|
||||||
"Sidebar.random-icons": "Icone casuali",
|
"Sidebar.random-icons": "Icone casuali",
|
||||||
"Sidebar.set-language": "Imposta la lingua",
|
"Sidebar.set-language": "Imposta la lingua",
|
||||||
"Sidebar.set-theme": "Imposta il tema",
|
"Sidebar.set-theme": "Imposta il tema",
|
||||||
"Sidebar.settings": "Impostazioni",
|
"Sidebar.settings": "Impostazioni",
|
||||||
|
"Sidebar.template-from-board": "Nuovo template dalla bacheca",
|
||||||
"Sidebar.untitled-board": "(Contenitore senza titolo)",
|
"Sidebar.untitled-board": "(Contenitore senza titolo)",
|
||||||
|
"SidebarCategories.BlocksMenu.Move": "Sposta a...",
|
||||||
|
"SidebarCategories.CategoryMenu.CreateNew": "Crea nuova categoria",
|
||||||
|
"SidebarCategories.CategoryMenu.Delete": "Elimina categoria",
|
||||||
|
"SidebarCategories.CategoryMenu.DeleteModal.Body": "Le bacheche in <b>{categoryName}</b> andranno nelle categorie precedenti. Non hai rimosso alcuna bacheca.",
|
||||||
|
"SidebarCategories.CategoryMenu.DeleteModal.Title": "Eliminare questa categoria?",
|
||||||
|
"SidebarCategories.CategoryMenu.Update": "Rinomina categoria",
|
||||||
"TableComponent.add-icon": "Aggiungi icona",
|
"TableComponent.add-icon": "Aggiungi icona",
|
||||||
"TableComponent.name": "Nome",
|
"TableComponent.name": "Nome",
|
||||||
"TableComponent.plus-new": "+ Nuovo",
|
"TableComponent.plus-new": "+ Nuovo",
|
||||||
@ -232,16 +275,30 @@
|
|||||||
"WelcomePage.Description": "Boards è uno strumento organizzativo per progetti che aiuta a definire, organizzare, tenere traccia e controllo del lavoro tra gruppi, usando una vista familiare a scheda Kanban",
|
"WelcomePage.Description": "Boards è uno strumento organizzativo per progetti che aiuta a definire, organizzare, tenere traccia e controllo del lavoro tra gruppi, usando una vista familiare a scheda Kanban",
|
||||||
"WelcomePage.Explore.Button": "Esplora",
|
"WelcomePage.Explore.Button": "Esplora",
|
||||||
"WelcomePage.Heading": "Benvenuto in Boards",
|
"WelcomePage.Heading": "Benvenuto in Boards",
|
||||||
|
"WelcomePage.NoThanks.Text": "No grazie, farò da solo",
|
||||||
"Workspace.editing-board-template": "Stai modificando un modello di una bacheca.",
|
"Workspace.editing-board-template": "Stai modificando un modello di una bacheca.",
|
||||||
"calendar.month": "Mese",
|
"calendar.month": "Mese",
|
||||||
"calendar.today": "OGGI",
|
"calendar.today": "OGGI",
|
||||||
"calendar.week": "Settimana",
|
"calendar.week": "Settimana",
|
||||||
"default-properties.badges": "Commenti e Descrizione",
|
"default-properties.badges": "Commenti e Descrizione",
|
||||||
"default-properties.title": "Titolo",
|
"default-properties.title": "Titolo",
|
||||||
|
"error.back-to-boards": "Ritorna alle boards",
|
||||||
|
"error.back-to-home": "Ritorna alla Home",
|
||||||
|
"error.go-login": "Accedi",
|
||||||
|
"error.not-logged-in": "La tua sessione potrebbe essere scaduta oppure non hai eseguito l'accesso",
|
||||||
|
"error.page.title": "Mi dispiace, qualcosa è andato storto",
|
||||||
"error.relogin": "Ricollegati",
|
"error.relogin": "Ricollegati",
|
||||||
|
"error.unknown": "È accaduto un errore.",
|
||||||
|
"error.workspace-undefined": "Workspace non valido.",
|
||||||
|
"generic.previous": "Precedente",
|
||||||
"login.log-in-button": "Login",
|
"login.log-in-button": "Login",
|
||||||
"login.log-in-title": "Login",
|
"login.log-in-title": "Login",
|
||||||
"login.register-button": "oppure crea un account se non ne hai già uno",
|
"login.register-button": "oppure crea un account se non ne hai già uno",
|
||||||
"register.login-button": "oppure fai il login se hai un account",
|
"register.login-button": "oppure fai il login se hai un account",
|
||||||
"register.signup-title": "Registrati per un tuo account"
|
"register.signup-title": "Registrati per un tuo account",
|
||||||
|
"shareBoard.lastAdmin": "Le bacheche devono avere almeno un amministratore",
|
||||||
|
"tutorial_tip.finish_tour": "Fatto",
|
||||||
|
"tutorial_tip.ok": "Prossimo",
|
||||||
|
"tutorial_tip.out": "Togli i suggerimenti",
|
||||||
|
"tutorial_tip.seen": "Hai mai visto questo prima d'ora?"
|
||||||
}
|
}
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Token opnieuw gegenereerd",
|
"ShareBoard.tokenRegenrated": "Token opnieuw gegenereerd",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Lid verwijderen",
|
"ShareBoard.userPermissionsRemoveMemberText": "Lid verwijderen",
|
||||||
"ShareBoard.userPermissionsYouText": "(jij)",
|
"ShareBoard.userPermissionsYouText": "(jij)",
|
||||||
|
"ShareTemplate.Title": "Sjabloon delen",
|
||||||
"Sidebar.about": "Over Focalboard",
|
"Sidebar.about": "Over Focalboard",
|
||||||
"Sidebar.add-board": "+ Bord toevoegen",
|
"Sidebar.add-board": "+ Bord toevoegen",
|
||||||
"Sidebar.changePassword": "Wachtwoord wijzigen",
|
"Sidebar.changePassword": "Wachtwoord wijzigen",
|
||||||
"Sidebar.delete-board": "Verwijder bord",
|
"Sidebar.delete-board": "Verwijder bord",
|
||||||
|
"Sidebar.duplicate-board": "Board dupliceren",
|
||||||
"Sidebar.export-archive": "Archief exporteren",
|
"Sidebar.export-archive": "Archief exporteren",
|
||||||
"Sidebar.import": "Importeren",
|
"Sidebar.import": "Importeren",
|
||||||
"Sidebar.import-archive": "Archief importeren",
|
"Sidebar.import-archive": "Archief importeren",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Taal instellen",
|
"Sidebar.set-language": "Taal instellen",
|
||||||
"Sidebar.set-theme": "Thema instellen",
|
"Sidebar.set-theme": "Thema instellen",
|
||||||
"Sidebar.settings": "Instellingen",
|
"Sidebar.settings": "Instellingen",
|
||||||
|
"Sidebar.template-from-board": "Nieuw sjabloon van board",
|
||||||
"Sidebar.untitled-board": "(Titelloze bord )",
|
"Sidebar.untitled-board": "(Titelloze bord )",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Verplaatsen naar...",
|
"SidebarCategories.BlocksMenu.Move": "Verplaatsen naar...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Maak een nieuwe categorie",
|
"SidebarCategories.CategoryMenu.CreateNew": "Maak een nieuwe categorie",
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Токен пересоздан",
|
"ShareBoard.tokenRegenrated": "Токен пересоздан",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Удалить участника",
|
"ShareBoard.userPermissionsRemoveMemberText": "Удалить участника",
|
||||||
"ShareBoard.userPermissionsYouText": "(Вы)",
|
"ShareBoard.userPermissionsYouText": "(Вы)",
|
||||||
|
"ShareTemplate.Title": "Поделиться Шаблоном",
|
||||||
"Sidebar.about": "О Focalboard",
|
"Sidebar.about": "О Focalboard",
|
||||||
"Sidebar.add-board": "+ Добавить доску",
|
"Sidebar.add-board": "+ Добавить доску",
|
||||||
"Sidebar.changePassword": "Изменить пароль",
|
"Sidebar.changePassword": "Изменить пароль",
|
||||||
"Sidebar.delete-board": "Удалить доску",
|
"Sidebar.delete-board": "Удалить доску",
|
||||||
|
"Sidebar.duplicate-board": "Дублировать доску",
|
||||||
"Sidebar.export-archive": "Экспорт архива",
|
"Sidebar.export-archive": "Экспорт архива",
|
||||||
"Sidebar.import": "Импорт",
|
"Sidebar.import": "Импорт",
|
||||||
"Sidebar.import-archive": "Импорт архива",
|
"Sidebar.import-archive": "Импорт архива",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Язык",
|
"Sidebar.set-language": "Язык",
|
||||||
"Sidebar.set-theme": "Тема",
|
"Sidebar.set-theme": "Тема",
|
||||||
"Sidebar.settings": "Настройки",
|
"Sidebar.settings": "Настройки",
|
||||||
|
"Sidebar.template-from-board": "Новый шаблон из доски",
|
||||||
"Sidebar.untitled-board": "(Доска без названия)",
|
"Sidebar.untitled-board": "(Доска без названия)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Перейти к...",
|
"SidebarCategories.BlocksMenu.Move": "Перейти к...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Создать новую категорию",
|
"SidebarCategories.CategoryMenu.CreateNew": "Создать новую категорию",
|
||||||
|
@ -195,10 +195,12 @@
|
|||||||
"ShareBoard.tokenRegenrated": "Kod yeniden oluşturuldu",
|
"ShareBoard.tokenRegenrated": "Kod yeniden oluşturuldu",
|
||||||
"ShareBoard.userPermissionsRemoveMemberText": "Üyelikten çıkar",
|
"ShareBoard.userPermissionsRemoveMemberText": "Üyelikten çıkar",
|
||||||
"ShareBoard.userPermissionsYouText": "(Siz)",
|
"ShareBoard.userPermissionsYouText": "(Siz)",
|
||||||
|
"ShareTemplate.Title": "Kalıbı paylaş",
|
||||||
"Sidebar.about": "Focalboard hakkında",
|
"Sidebar.about": "Focalboard hakkında",
|
||||||
"Sidebar.add-board": "+ Pano ekle",
|
"Sidebar.add-board": "+ Pano ekle",
|
||||||
"Sidebar.changePassword": "Parola değiştir",
|
"Sidebar.changePassword": "Parola değiştir",
|
||||||
"Sidebar.delete-board": "Panoyu sil",
|
"Sidebar.delete-board": "Panoyu sil",
|
||||||
|
"Sidebar.duplicate-board": "Panoyu kopyala",
|
||||||
"Sidebar.export-archive": "Arşivi dışa aktar",
|
"Sidebar.export-archive": "Arşivi dışa aktar",
|
||||||
"Sidebar.import": "İçe aktar",
|
"Sidebar.import": "İçe aktar",
|
||||||
"Sidebar.import-archive": "Arşivi içe aktar",
|
"Sidebar.import-archive": "Arşivi içe aktar",
|
||||||
@ -209,6 +211,7 @@
|
|||||||
"Sidebar.set-language": "Dil ayarla",
|
"Sidebar.set-language": "Dil ayarla",
|
||||||
"Sidebar.set-theme": "Tema ayarla",
|
"Sidebar.set-theme": "Tema ayarla",
|
||||||
"Sidebar.settings": "Ayarlar",
|
"Sidebar.settings": "Ayarlar",
|
||||||
|
"Sidebar.template-from-board": "Panodan yeni kalıp",
|
||||||
"Sidebar.untitled-board": "(Başlıksız pano)",
|
"Sidebar.untitled-board": "(Başlıksız pano)",
|
||||||
"SidebarCategories.BlocksMenu.Move": "Şuraya taşı...",
|
"SidebarCategories.BlocksMenu.Move": "Şuraya taşı...",
|
||||||
"SidebarCategories.CategoryMenu.CreateNew": "Yeni kategori ekle",
|
"SidebarCategories.CategoryMenu.CreateNew": "Yeni kategori ekle",
|
||||||
|
@ -11,8 +11,8 @@ import {IUser} from '../../user'
|
|||||||
import FocalboardLogoIcon from '../../widgets/icons/focalboard_logo'
|
import FocalboardLogoIcon from '../../widgets/icons/focalboard_logo'
|
||||||
import Menu from '../../widgets/menu'
|
import Menu from '../../widgets/menu'
|
||||||
import MenuWrapper from '../../widgets/menuWrapper'
|
import MenuWrapper from '../../widgets/menuWrapper'
|
||||||
import {getMe} from '../../store/users'
|
import {getMe, setMe} from '../../store/users'
|
||||||
import {useAppSelector} from '../../store/hooks'
|
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
import ModalWrapper from '../modalWrapper'
|
import ModalWrapper from '../modalWrapper'
|
||||||
@ -26,6 +26,7 @@ import './sidebarUserMenu.scss'
|
|||||||
declare let window: IAppWindow
|
declare let window: IAppWindow
|
||||||
|
|
||||||
const SidebarUserMenu = () => {
|
const SidebarUserMenu = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const [showRegistrationLinkDialog, setShowRegistrationLinkDialog] = useState(false)
|
const [showRegistrationLinkDialog, setShowRegistrationLinkDialog] = useState(false)
|
||||||
const user = useAppSelector<IUser|null>(getMe)
|
const user = useAppSelector<IUser|null>(getMe)
|
||||||
@ -60,6 +61,7 @@ const SidebarUserMenu = () => {
|
|||||||
name={intl.formatMessage({id: 'Sidebar.logout', defaultMessage: 'Log out'})}
|
name={intl.formatMessage({id: 'Sidebar.logout', defaultMessage: 'Log out'})}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await octoClient.logout()
|
await octoClient.logout()
|
||||||
|
dispatch(setMe(null))
|
||||||
history.push('/login')
|
history.push('/login')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
// 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.
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
|
import {Link, Redirect, useLocation, useHistory} from 'react-router-dom'
|
||||||
import {Link, useLocation, useHistory} from 'react-router-dom'
|
|
||||||
import {FormattedMessage} from 'react-intl'
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
import {useAppDispatch} from '../store/hooks'
|
import {useAppDispatch, useAppSelector} from '../store/hooks'
|
||||||
import {fetchMe} from '../store/users'
|
import {fetchMe, getLoggedIn} from '../store/users'
|
||||||
|
|
||||||
import Button from '../widgets/buttons/button'
|
import Button from '../widgets/buttons/button'
|
||||||
import client from '../octoClient'
|
import client from '../octoClient'
|
||||||
@ -17,6 +16,7 @@ const LoginPage = () => {
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||||
const queryParams = new URLSearchParams(useLocation().search)
|
const queryParams = new URLSearchParams(useLocation().search)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
@ -34,6 +34,10 @@ const LoginPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loggedIn) {
|
||||||
|
return <Redirect to={'/'}/>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='LoginPage'>
|
<div className='LoginPage'>
|
||||||
<form
|
<form
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// 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.
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {useHistory, Link} from 'react-router-dom'
|
import {useHistory, Link, Redirect} from 'react-router-dom'
|
||||||
import {FormattedMessage} from 'react-intl'
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
import {useAppDispatch} from '../store/hooks'
|
import {useAppDispatch, useAppSelector} from '../store/hooks'
|
||||||
import {fetchMe} from '../store/users'
|
import {fetchMe, getLoggedIn} from '../store/users'
|
||||||
|
|
||||||
import Button from '../widgets/buttons/button'
|
import Button from '../widgets/buttons/button'
|
||||||
import client from '../octoClient'
|
import client from '../octoClient'
|
||||||
@ -18,6 +18,7 @@ const RegisterPage = () => {
|
|||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
|
||||||
|
|
||||||
const handleRegister = async (): Promise<void> => {
|
const handleRegister = async (): Promise<void> => {
|
||||||
const queryString = new URLSearchParams(window.location.search)
|
const queryString = new URLSearchParams(window.location.search)
|
||||||
@ -37,6 +38,10 @@ const RegisterPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loggedIn) {
|
||||||
|
return <Redirect to={'/'}/>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='RegisterPage'>
|
<div className='RegisterPage'>
|
||||||
<form
|
<form
|
||||||
|
@ -44,8 +44,9 @@ const usersSlice = createSlice({
|
|||||||
name: 'users',
|
name: 'users',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setMe: (state, action: PayloadAction<IUser>) => {
|
setMe: (state, action: PayloadAction<IUser|null>) => {
|
||||||
state.me = action.payload
|
state.me = action.payload
|
||||||
|
state.loggedIn = Boolean(state.me)
|
||||||
},
|
},
|
||||||
setBoardUsers: (state, action: PayloadAction<IUser[]>) => {
|
setBoardUsers: (state, action: PayloadAction<IUser[]>) => {
|
||||||
state.boardUsers = action.payload.reduce((acc: {[key: string]: IUser}, user: IUser) => {
|
state.boardUsers = action.payload.reduce((acc: {[key: string]: IUser}, user: IUser) => {
|
||||||
|
Reference in New Issue
Block a user